Compare commits

...

10 Commits

Author SHA1 Message Date
Johannes 569285bdd2 add plan 2026-03-24 17:32:54 +01:00
Dhruwang Jariwala 474be86d33 fix: translations for option types (#7576) 2026-03-24 13:18:26 +00:00
Dhruwang Jariwala e7ca66ed77 fix: use TTC data for reliable survey impression counting (#7572) 2026-03-24 08:52:35 +00:00
Matti Nannt 2b49dbecd3 chore: add dev:setup script to generate .env and missing secrets (#7555)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2026-03-24 08:26:32 +00:00
Anshuman Pandey 6da4c6f352 fix: proper errors server side when resources are not found (#7571) 2026-03-24 07:52:37 +00:00
Aryan Ghugare 659b240fca feat: enhance welcome card to support video uploads and display #7491 (#7497)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2026-03-24 07:34:43 +00:00
Dhruwang Jariwala 19c0b1d14d fix: response table settings formatting (#7540)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2026-03-24 06:36:45 +00:00
Dhruwang Jariwala b4472f48e9 fix: (Duplicate) prevent multi-language survey buttons from falling back to English (#7559) 2026-03-24 05:45:47 +00:00
bharath kumar d197271771 fix(web): add <noscript> message for when JS is disabled (#7455) (#7459)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2026-03-23 12:35:29 +00:00
Dhruwang Jariwala 37f652c70e fix: prevent session expiry during active use (#7558) 2026-03-23 10:44:55 +00:00
89 changed files with 750 additions and 516 deletions
@@ -0,0 +1,83 @@
---
name: inherit-font-toggle-plan
overview: Add an easy “inherit host page font” design using the existing `fontFamily` styling field, mapped to CSS variable fallback behavior in the SDK. Keep it backward-compatible and minimize schema churn by avoiding new persisted fields.
todos:
- id: sdk-font-fallback
content: Switch SDK preflight font-family to var(--fb-font-family, inherit) in surveys preflight CSS.
status: pending
- id: inject-font-variable
content: Wire styling.fontFamily into addCustomThemeToDom to emit --fb-font-family only when set.
status: pending
- id: editor-toggle-ui
content: Add inherit-font toggle + conditional font stack input in shared FormStylingSettings component.
status: pending
- id: i18n-keys
content: Add English translation keys for new toggle and font stack field text.
status: pending
- id: tests
content: Add/extend tests for CSS variable emission and UI toggle-to-fontFamily mapping.
status: pending
- id: manual-verification
content: Validate inline/modal behavior with host-font inherit ON and explicit stack OFF.
status: pending
isProject: false
---
# Inherit Font Toggle (Easy Design)
## Goal
Implement a simple styling toggle that lets users choose whether surveys inherit the host page font, using existing `fontFamily` (no new DB/schema field).
## Implementation Approach
- Represent toggle state via `fontFamily`:
- **Inherit ON**: `fontFamily` is `null`/unset
- **Inherit OFF**: `fontFamily` contains a font stack string
- Make SDK preflight use a CSS variable fallback:
- `font-family: var(--fb-font-family, inherit);`
- Inject `--fb-font-family` only when `styling.fontFamily` is set.
## Changes by Area
- **SDK base font behavior**
- Update [packages/surveys/src/styles/preflight.css](packages/surveys/src/styles/preflight.css) to replace hardcoded Inter stack with variable + inherit fallback.
- **Theme/style variable injection**
- Update [packages/surveys/src/lib/styles.ts](packages/surveys/src/lib/styles.ts) to append `--fb-font-family` from `styling.fontFamily` when present.
- **Styling editor UX (workspace + survey reuse)**
- Extend [apps/web/modules/survey/editor/components/form-styling-settings.tsx](apps/web/modules/survey/editor/components/form-styling-settings.tsx):
- Add a toggle control for “Inherit font from host page”.
- When disabled, show a text field for font stack (bind to `fontFamily`).
- Keep this inside advanced styling section to reduce UI noise.
- Because workspace theme and survey styling both reuse this component, this covers both entry points (including [apps/web/modules/projects/settings/look/components/theme-styling.tsx](apps/web/modules/projects/settings/look/components/theme-styling.tsx) and survey editor views) without duplicating UI code.
- **Defaults and compatibility**
- Ensure defaults continue to behave as inherit when `fontFamily` is absent (no mandatory updates to defaults object needed).
- Verify existing saved stylings without `fontFamily` continue to render unchanged except adopting host font (intended behavior).
- **Translations**
- Add new i18n keys in [apps/web/locales/en-US.json](apps/web/locales/en-US.json) for the toggle label/description and custom-font input label/description.
## Testing Plan
- Extend [packages/surveys/src/lib/styles.test.ts](packages/surveys/src/lib/styles.test.ts):
- Assert `--fb-font-family` is emitted when `fontFamily` is provided.
- Assert it is omitted when `fontFamily` is null/undefined.
- Add/adjust editor component tests (where existing pattern for form controls exists) to verify toggle behavior updates `fontFamily` correctly.
- Manual verification:
- Host page with a distinctive font: confirm survey inherits when toggle ON.
- Toggle OFF + custom stack: confirm survey uses configured stack.
- Regression check in modal and inline renders.
## Risks and Mitigations
- **Risk:** Host font may be unavailable in some contexts.
- **Mitigation:** Custom stack path remains available when inherit is OFF.
- **Risk:** Confusion between workspace and survey-level overrides.
- **Mitigation:** Keep existing overwrite semantics; only map toggle to `fontFamily` value at the currently edited scope.
- **Risk:** Iframe embeds cannot inherit outer-page font.
- **Mitigation:** Document that iframe use requires explicit font stack (toggle OFF).
## Validation Commands (post-implementation)
- `pnpm test --filter @formbricks/surveys`
- `pnpm lint`
- Optional manual check in a host app with a non-default font to verify inheritance behavior.
@@ -1,5 +1,6 @@
import { XIcon } from "lucide-react";
import Link from "next/link";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
import { getEnvironment } from "@/lib/environment/service";
import { getPublicDomain } from "@/lib/getPublicUrl";
@@ -20,12 +21,12 @@ const Page = async (props: ConnectPageProps) => {
const environment = await getEnvironment(params.environmentId);
if (!environment) {
throw new Error(t("common.environment_not_found"));
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
}
const project = await getProjectByEnvironmentId(environment.id);
if (!project) {
throw new Error(t("common.workspace_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
const channel = project.config.channel || null;
@@ -1,6 +1,7 @@
import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import Link from "next/link";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
import { getEnvironment } from "@/lib/environment/service";
import { getProjectByEnvironmentId, getUserProjects } from "@/lib/project/service";
@@ -23,22 +24,22 @@ const Page = async (props: XMTemplatePageProps) => {
const environment = await getEnvironment(params.environmentId);
const t = await getTranslate();
if (!session) {
throw new Error(t("common.session_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
const user = await getUser(session.user.id);
if (!user) {
throw new Error(t("common.user_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
if (!environment) {
throw new Error(t("common.environment_not_found"));
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
}
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
const project = await getProjectByEnvironmentId(environment.id);
if (!project) {
throw new Error(t("common.workspace_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
const projects = await getUserProjects(session.user.id, organizationId);
@@ -1,6 +1,6 @@
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { AuthorizationError } from "@formbricks/types/errors";
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { canUserAccessOrganization } from "@/lib/organization/auth";
import { getOrganization } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
@@ -25,7 +25,7 @@ const ProjectOnboardingLayout = async (props: {
const user = await getUser(session.user.id);
if (!user) {
throw new Error(t("common.user_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
const isAuthorized = await canUserAccessOrganization(session.user.id, params.organizationId);
@@ -36,7 +36,7 @@ const ProjectOnboardingLayout = async (props: {
const organization = await getOrganization(params.organizationId);
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), params.organizationId);
}
return (
@@ -1,5 +1,6 @@
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import { getOrganization } from "@/lib/organization/service";
@@ -28,7 +29,7 @@ const OnboardingLayout = async (props: {
const organization = await getOrganization(params.organizationId);
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), params.organizationId);
}
const [organizationProjectsLimit, organizationProjectsCount] = await Promise.all([
@@ -1,6 +1,7 @@
import { XIcon } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/settings/components/ProjectSettings";
@@ -45,7 +46,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
const isAccessControlAllowed = await getAccessControlPermission(organization.id);
if (!organizationTeams) {
throw new Error(t("common.organization_teams_not_found"));
throw new ResourceNotFoundError(t("common.team"), null);
}
const publicDomain = getPublicDomain();
@@ -1,4 +1,5 @@
import { redirect } from "next/navigation";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getEnvironment } from "@/lib/environment/service";
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
@@ -17,13 +18,13 @@ const SurveyEditorEnvironmentLayout = async (props: {
}
if (!user) {
throw new Error(t("common.user_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
const environment = await getEnvironment(params.environmentId);
if (!environment) {
throw new Error(t("common.environment_not_found"));
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
}
return (
@@ -2,7 +2,11 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
import {
AuthorizationError,
OperationNotAllowedError,
ResourceNotFoundError,
} from "@formbricks/types/errors";
import { ZProjectUpdateInput } from "@formbricks/types/project";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getOrganization } from "@/lib/organization/service";
@@ -46,7 +50,7 @@ export const createProjectAction = authenticatedActionClient.inputSchema(ZCreate
const organization = await getOrganization(organizationId);
if (!organization) {
throw new Error("Organization not found");
throw new ResourceNotFoundError("Organization", organizationId);
}
const organizationProjectsLimit = await getOrganizationProjectsLimit(organization.id);
@@ -1,3 +1,4 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
@@ -42,7 +43,7 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
// Validate that project permission exists for members
if (isMember && !projectPermission) {
throw new Error(t("common.workspace_permission_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
return (
@@ -1,4 +1,5 @@
import { getServerSession } from "next-auth";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { getTranslate } from "@/lingodotdev/server";
@@ -20,15 +21,15 @@ const AccountSettingsLayout = async (props: {
]);
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), null);
}
if (!project) {
throw new Error(t("common.workspace_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
if (!session) {
throw new Error(t("common.session_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
return <>{children}</>;
@@ -1,5 +1,6 @@
import { getServerSession } from "next-auth";
import { prisma } from "@formbricks/database";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TUserNotificationSettings } from "@formbricks/types/user";
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
@@ -146,18 +147,18 @@ const Page = async (props: {
const t = await getTranslate();
const session = await getServerSession(authOptions);
if (!session) {
throw new Error(t("common.session_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
const autoDisableNotificationType = searchParams["type"];
const autoDisableNotificationElementId = searchParams["elementId"];
const [user, memberships] = await Promise.all([getUser(session.user.id), getMemberships(session.user.id)]);
if (!user) {
throw new Error(t("common.user_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
if (!memberships) {
throw new Error(t("common.membership_not_found"));
throw new ResourceNotFoundError(t("common.membership"), null);
}
if (user?.notificationSettings) {
@@ -1,3 +1,4 @@
import { AuthenticationError } from "@formbricks/types/errors";
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD, PASSWORD_RESET_DISABLED } from "@/lib/constants";
@@ -28,7 +29,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const user = session?.user ? await getUser(session.user.id) : null;
if (!user) {
throw new Error(t("common.user_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
const isPasswordResetEnabled = !PASSWORD_RESET_DISABLED && user.identityProvider === "email";
@@ -1,4 +1,5 @@
import { notFound } from "next/navigation";
import { AuthenticationError } from "@formbricks/types/errors";
import { IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
@@ -25,7 +26,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
);
if (!session) {
throw new Error(t("common.session_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
const hasWhiteLabelPermission = await getWhiteLabelPermission(organization.id);
@@ -1,4 +1,5 @@
import { getServerSession } from "next-auth";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { getTranslate } from "@/lingodotdev/server";
@@ -17,15 +18,15 @@ const Layout = async (props: { params: Promise<{ environmentId: string }>; child
]);
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), null);
}
if (!project) {
throw new Error(t("common.workspace_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
if (!session) {
throw new Error(t("common.session_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
return <>{children}</>;
@@ -300,7 +300,6 @@ export const ResponseTable = ({
<DataTableSettingsModal
open={isTableSettingsModalOpen}
setOpen={setIsTableSettingsModalOpen}
survey={survey}
table={table}
columnOrder={columnOrder}
handleDragEnd={handleDragEnd}
@@ -1,3 +1,4 @@
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
@@ -31,15 +32,15 @@ const Page = async (props: { params: Promise<{ environmentId: string; surveyId:
]);
if (!survey) {
throw new Error(t("common.survey_not_found"));
throw new ResourceNotFoundError(t("common.survey"), params.surveyId);
}
if (!user) {
throw new Error(t("common.user_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), null);
}
const segments = isContactsEnabled ? await getSegments(params.environmentId) : [];
@@ -48,7 +49,7 @@ const Page = async (props: { params: Promise<{ environmentId: string; surveyId:
const organizationBilling = await getOrganizationBilling(organization.id);
if (!organizationBilling) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), organization.id);
}
const isQuotasAllowed = await getIsQuotasEnabled(organization.id);
@@ -1,3 +1,4 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getPublicDomain } from "@/lib/getPublicUrl";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { getSurvey } from "@/lib/survey/service";
@@ -9,11 +10,11 @@ export const getEmailTemplateHtml = async (surveyId: string, locale: string) =>
const t = await getTranslate();
const survey = await getSurvey(surveyId);
if (!survey) {
throw new Error("Survey not found");
throw new ResourceNotFoundError(t("common.survey"), surveyId);
}
const project = await getProjectByEnvironmentId(survey.environmentId);
if (!project) {
throw new Error("Workspace not found");
throw new ResourceNotFoundError(t("common.workspace"), null);
}
const styling = getStyling(project, survey);
@@ -11,8 +11,7 @@ import { getDisplayCountBySurveyId } from "@/lib/display/service";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { getResponseCountBySurveyId } from "@/lib/response/service";
import { getSurvey } from "@/lib/survey/service";
import { evaluateLogic, performActions } from "@/lib/surveyLogic/utils";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { getElementsFromBlocks } from "@/lib/survey/utils";
import {
getElementSummary,
getResponsesForSummary,
@@ -44,7 +43,7 @@ vi.mock("@/lib/survey/service", () => ({
}));
vi.mock("@/lib/surveyLogic/utils", () => ({
evaluateLogic: vi.fn(),
performActions: vi.fn(() => ({ jumpTarget: undefined, requiredQuestionIds: [], calculations: {} })),
performActions: vi.fn(() => ({ jumpTarget: undefined, requiredElementIds: [], calculations: {} })),
}));
vi.mock("@/lib/utils/validate", () => ({
validateInputs: vi.fn(),
@@ -229,12 +228,6 @@ describe("getSurveySummaryDropOff", () => {
vi.mocked(convertFloatTo2Decimal).mockImplementation((num) =>
num !== undefined && num !== null ? parseFloat(num.toFixed(2)) : 0
);
vi.mocked(evaluateLogic).mockReturnValue(false); // Default: no logic triggers
vi.mocked(performActions).mockReturnValue({
jumpTarget: undefined,
requiredElementIds: [],
calculations: {},
});
});
test("calculates dropOff correctly with welcome card disabled", () => {
@@ -246,7 +239,7 @@ describe("getSurveySummaryDropOff", () => {
contact: null,
contactAttributes: {},
language: "en",
ttc: { q1: 10 },
ttc: { q1: 10, q2: 5 }, // Saw q2 but didn't answer it
finished: false,
}, // Dropped at q2
{
@@ -269,22 +262,55 @@ describe("getSurveySummaryDropOff", () => {
);
expect(dropOff.length).toBe(2);
// Q1
// Q1: welcome card disabled so impressions = displayCount
expect(dropOff[0].elementId).toBe("q1");
expect(dropOff[0].impressions).toBe(displayCount); // Welcome card disabled, so first question impressions = displayCount
expect(dropOff[0].impressions).toBe(displayCount);
expect(dropOff[0].dropOffCount).toBe(displayCount - responses.length); // 5 displays - 2 started = 3 dropped before q1
expect(dropOff[0].dropOffPercentage).toBe(60); // (3/5)*100
expect(dropOff[0].ttc).toBe(10);
// Q2
// Q2: both responses saw q2 (r1 has ttc for q2, r2 answered q2)
expect(dropOff[1].elementId).toBe("q2");
expect(dropOff[1].impressions).toBe(responses.length); // 2 responses reached q1, so 2 impressions for q2
expect(dropOff[1].dropOffCount).toBe(1); // 1 response dropped at q2
expect(dropOff[1].impressions).toBe(2);
expect(dropOff[1].dropOffCount).toBe(1); // r1 dropped at q2 (last seen element)
expect(dropOff[1].dropOffPercentage).toBe(50); // (1/2)*100
expect(dropOff[1].ttc).toBe(10);
expect(dropOff[1].ttc).toBe(7.5); // avg of r1(5ms) and r2(10ms)
});
test("handles logic jumps", () => {
test("drop-off attributed to last seen element when user doesn't reach next question", () => {
// Welcome card enabled so first element drop-off is NOT overridden by displayCount
const surveyWithWelcome: TSurvey = {
...surveyWithBlocks,
welcomeCard: { enabled: true, headline: { default: "Welcome" } } as unknown as TSurvey["welcomeCard"],
};
const responses = [
{
id: "r1",
data: { q1: "a" },
updatedAt: new Date(),
contact: null,
contactAttributes: {},
language: "en",
ttc: { q1: 10 }, // Only saw q1, never reached q2
finished: false,
},
] as any;
const displayCount = 1;
const dropOff = getSurveySummaryDropOff(
surveyWithWelcome,
getElementsFromBlocks(surveyWithWelcome.blocks),
responses,
displayCount
);
expect(dropOff[0].impressions).toBe(1); // Saw q1
expect(dropOff[0].dropOffCount).toBe(1); // Dropped at q1 (last seen element)
expect(dropOff[1].impressions).toBe(0); // Never saw q2
expect(dropOff[1].dropOffCount).toBe(0);
});
test("handles logic jumps — impressions based on actual ttc/data, not logic replay", () => {
// Survey with 4 questions across 4 blocks, logic on block2 jumps q2->q4 (skipping q3)
const surveyWithLogic: TSurvey = {
...mockBaseSurvey,
blocks: [
@@ -315,36 +341,6 @@ describe("getSurveySummaryDropOff", () => {
charLimit: { enabled: false },
},
] as TSurveyElement[],
logic: [
{
id: "logic1",
conditions: {
id: "condition1",
connector: "and" as const,
conditions: [
{
id: "c1",
leftOperand: {
type: "element" as const,
value: "q2",
},
operator: "equals" as const,
rightOperand: {
type: "static" as const,
value: "b",
},
},
],
},
actions: [
{
id: "action1",
objective: "jumpToBlock" as const,
target: "q4",
},
],
},
],
},
{
id: "block3",
@@ -377,28 +373,21 @@ describe("getSurveySummaryDropOff", () => {
],
questions: [],
};
// Response where user answered q1, q2, then logic jumped to q4 (skipping q3).
// The ttc/data reflects exactly what elements were shown — no logic replay needed.
const responses = [
{
id: "r1",
data: { q1: "a", q2: "b" },
data: { q1: "a", q2: "b", q4: "d" },
updatedAt: new Date(),
contact: null,
contactAttributes: {},
language: "en",
ttc: { q1: 10, q2: 10 },
ttc: { q1: 10, q2: 10, q4: 10 }, // q3 has no ttc entry — was skipped by logic
finished: false,
}, // Jumps from q2 to q4, drops at q4
},
];
vi.mocked(evaluateLogic).mockImplementation((_s, data, _v, _, _l) => {
// Simulate logic on q2 triggering
return data.q2 === "b";
});
vi.mocked(performActions).mockImplementation((_s, actions, _d, _v) => {
if (actions[0] && "objective" in actions[0] && actions[0].objective === "jumpToBlock") {
return { jumpTarget: actions[0].target, requiredElementIds: [], calculations: {} };
}
return { jumpTarget: undefined, requiredElementIds: [], calculations: {} };
});
const dropOff = getSurveySummaryDropOff(
surveyWithLogic,
@@ -407,11 +396,11 @@ describe("getSurveySummaryDropOff", () => {
1
);
expect(dropOff[0].impressions).toBe(1); // q1
expect(dropOff[1].impressions).toBe(1); // q2
expect(dropOff[2].impressions).toBe(0); // q3 (skipped)
expect(dropOff[3].impressions).toBe(1); // q4 (jumped to)
expect(dropOff[3].dropOffCount).toBe(1); // Dropped at q4
expect(dropOff[0].impressions).toBe(1); // q1: seen
expect(dropOff[1].impressions).toBe(1); // q2: seen
expect(dropOff[2].impressions).toBe(0); // q3: skipped by logic (no ttc, no data)
expect(dropOff[3].impressions).toBe(1); // q4: jumped to, seen
expect(dropOff[3].dropOffCount).toBe(1); // Dropped at q4 (last seen element, not finished)
});
});
@@ -11,7 +11,6 @@ import {
TResponseData,
TResponseFilterCriteria,
TResponseTtc,
TResponseVariables,
ZResponseFilterCriteria,
} from "@formbricks/types/responses";
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
@@ -37,8 +36,7 @@ import { getDisplayCountBySurveyId } from "@/lib/display/service";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { buildWhereClause } from "@/lib/response/utils";
import { getSurvey } from "@/lib/survey/service";
import { findElementLocation, getElementsFromBlocks } from "@/lib/survey/utils";
import { evaluateLogic, performActions } from "@/lib/surveyLogic/utils";
import { getElementsFromBlocks } from "@/lib/survey/utils";
import { validateInputs } from "@/lib/utils/validate";
import { convertFloatTo2Decimal } from "./utils";
@@ -93,63 +91,13 @@ export const getSurveySummaryMeta = (
};
};
const evaluateLogicAndGetNextElementId = (
localSurvey: TSurvey,
elements: TSurveyElement[],
data: TResponseData,
localVariables: TResponseVariables,
currentElementIndex: number,
currElementTemp: TSurveyElement,
selectedLanguage: string | null
): {
nextElementId: string | undefined;
updatedSurvey: TSurvey;
updatedVariables: TResponseVariables;
} => {
let updatedSurvey = { ...localSurvey };
let updatedVariables = { ...localVariables };
let firstJumpTarget: string | undefined;
const { block: currentBlock } = findElementLocation(localSurvey, currElementTemp.id);
if (currentBlock?.logic && currentBlock.logic.length > 0) {
for (const logic of currentBlock.logic) {
if (evaluateLogic(localSurvey, data, localVariables, logic.conditions, selectedLanguage ?? "default")) {
const { jumpTarget, requiredElementIds, calculations } = performActions(
updatedSurvey,
logic.actions,
data,
updatedVariables
);
if (requiredElementIds.length > 0) {
// Update blocks to mark elements as required
updatedSurvey.blocks = updatedSurvey.blocks.map((block) => ({
...block,
elements: block.elements.map((e) =>
requiredElementIds.includes(e.id) ? { ...e, required: true } : e
),
}));
}
updatedVariables = { ...updatedVariables, ...calculations };
if (jumpTarget && !firstJumpTarget) {
firstJumpTarget = jumpTarget;
}
}
}
}
// If no jump target was set, check for a fallback logic
if (!firstJumpTarget && currentBlock?.logicFallback) {
firstJumpTarget = currentBlock.logicFallback;
}
// Return the first jump target if found, otherwise go to the next element
const nextElementId = firstJumpTarget || elements[currentElementIndex + 1]?.id || undefined;
return { nextElementId, updatedSurvey, updatedVariables };
// Determine whether a response interacted with a given element.
// An element was "seen" if the respondent has a ttc entry for it OR provided an answer.
// This is more reliable than replaying survey logic, which can misattribute impressions
// when branching logic skips elements or when partial response data is insufficient
// to evaluate conditions correctly.
const wasElementSeen = (response: TSurveySummaryResponse, elementId: string): boolean => {
return (response.ttc != null && response.ttc[elementId] > 0) || response.data[elementId] !== undefined;
};
export const getSurveySummaryDropOff = (
@@ -170,16 +118,8 @@ export const getSurveySummaryDropOff = (
let impressionsArr = new Array(elements.length).fill(0) as number[];
let dropOffPercentageArr = new Array(elements.length).fill(0) as number[];
const surveyVariablesData = survey.variables?.reduce(
(acc, variable) => {
acc[variable.id] = variable.value;
return acc;
},
{} as Record<string, string | number>
);
responses.forEach((response) => {
// Calculate total time-to-completion
// Calculate total time-to-completion per element
Object.keys(totalTtc).forEach((elementId) => {
if (response.ttc && response.ttc[elementId]) {
totalTtc[elementId] += response.ttc[elementId];
@@ -187,51 +127,21 @@ export const getSurveySummaryDropOff = (
}
});
let localSurvey = structuredClone(survey);
let localResponseData: TResponseData = { ...response.data };
let localVariables: TResponseVariables = {
...surveyVariablesData,
};
// Count impressions based on actual interaction data (ttc + response data)
// instead of replaying survey logic which is unreliable with branching
let lastSeenIdx = -1;
let currQuesIdx = 0;
while (currQuesIdx < elements.length) {
const currQues = elements[currQuesIdx];
if (!currQues) break;
// element is not answered and required
if (response.data[currQues.id] === undefined && currQues.required) {
dropOffArr[currQuesIdx]++;
impressionsArr[currQuesIdx]++;
break;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (wasElementSeen(response, element.id)) {
impressionsArr[i]++;
lastSeenIdx = i;
}
}
impressionsArr[currQuesIdx]++;
const { nextElementId, updatedSurvey, updatedVariables } = evaluateLogicAndGetNextElementId(
localSurvey,
elements,
localResponseData,
localVariables,
currQuesIdx,
currQues,
response.language
);
localSurvey = updatedSurvey;
localVariables = updatedVariables;
if (nextElementId) {
const nextQuesIdx = elements.findIndex((q) => q.id === nextElementId);
if (!response.data[nextElementId] && !response.finished) {
dropOffArr[nextQuesIdx]++;
impressionsArr[nextQuesIdx]++;
break;
}
currQuesIdx = nextQuesIdx;
} else {
currQuesIdx++;
}
// Attribute drop-off to the last element the respondent interacted with
if (!response.finished && lastSeenIdx >= 0) {
dropOffArr[lastSeenIdx]++;
}
});
@@ -240,6 +150,8 @@ export const getSurveySummaryDropOff = (
totalTtc[elementId] = responseCounts[elementId] > 0 ? totalTtc[elementId] / responseCounts[elementId] : 0;
});
// When the welcome card is disabled, the first element's impressions should equal displayCount
// because every survey display is an impression of the first element
if (!survey.welcomeCard.enabled) {
dropOffArr[0] = displayCount - impressionsArr[0];
if (impressionsArr[0] > displayCount) dropOffPercentageArr[0] = 0;
@@ -251,7 +163,7 @@ export const getSurveySummaryDropOff = (
impressionsArr[0] = displayCount;
} else {
dropOffPercentageArr[0] = (dropOffArr[0] / impressionsArr[0]) * 100;
dropOffPercentageArr[0] = impressionsArr[0] > 0 ? (dropOffArr[0] / impressionsArr[0]) * 100 : 0;
}
for (let i = 1; i < elements.length; i++) {
@@ -1,4 +1,5 @@
import { notFound } from "next/navigation";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage";
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
@@ -32,13 +33,13 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
const survey = await getSurvey(params.surveyId);
if (!survey) {
throw new Error(t("common.survey_not_found"));
throw new ResourceNotFoundError(t("common.survey"), params.surveyId);
}
const user = await getUser(session.user.id);
if (!user) {
throw new Error(t("common.user_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
@@ -46,11 +47,11 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
const segments = isContactsEnabled ? await getSegments(environment.id) : [];
if (!organizationId) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), null);
}
const organizationBilling = await getOrganizationBilling(organizationId);
if (!organizationBilling) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), organizationId);
}
const isQuotasAllowed = await getIsQuotasEnabled(organizationId);
@@ -1,6 +1,7 @@
"use client";
import clsx from "clsx";
import { TFunction } from "i18next";
import {
AirplayIcon,
ArrowUpFromDotIcon,
@@ -54,6 +55,25 @@ export enum OptionsType {
QUOTAS = "Quotas",
}
const getOptionsTypeTranslationKey = (type: OptionsType, t: TFunction): string => {
switch (type) {
case OptionsType.ELEMENTS:
return t("common.elements");
case OptionsType.TAGS:
return t("common.tags");
case OptionsType.ATTRIBUTES:
return t("common.attributes");
case OptionsType.OTHERS:
return t("common.other_filters");
case OptionsType.META:
return t("common.meta");
case OptionsType.HIDDEN_FIELDS:
return t("common.hidden_fields");
case OptionsType.QUOTAS:
return t("common.quotas");
}
};
export type ElementOption = {
label: string;
elementType?: TSurveyElementTypeEnum;
@@ -218,7 +238,12 @@ export const ElementsComboBox = ({ options, selected, onChangeValue }: ElementCo
{options?.map((data) => (
<Fragment key={data.header}>
{data?.option.length > 0 && (
<CommandGroup heading={<p className="text-sm font-medium text-slate-600">{data.header}</p>}>
<CommandGroup
heading={
<p className="text-sm font-medium text-slate-600">
{getOptionsTypeTranslationKey(data.header, t)}
</p>
}>
{data?.option?.map((o) => (
<CommandItem
key={o.id}
@@ -1,4 +1,6 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getSurvey } from "@/lib/survey/service";
import { getTranslate } from "@/lingodotdev/server";
import { SurveyContextWrapper } from "./context/survey-context";
interface SurveyLayoutProps {
@@ -10,9 +12,10 @@ const SurveyLayout = async ({ params, children }: SurveyLayoutProps) => {
const resolvedParams = await params;
const survey = await getSurvey(resolvedParams.surveyId);
const t = await getTranslate();
if (!survey) {
throw new Error("Survey not found");
throw new ResourceNotFoundError(t("common.survey"), resolvedParams.surveyId);
}
return <SurveyContextWrapper survey={survey}>{children}</SurveyContextWrapper>;
+4 -2
View File
@@ -6,8 +6,10 @@ import {
CHATWOOT_WEBSITE_TOKEN,
IS_CHATWOOT_CONFIGURED,
POSTHOG_KEY,
SESSION_MAX_AGE,
} from "@/lib/constants";
import { getUser } from "@/lib/user/service";
import { NextAuthProvider } from "@/modules/auth/components/next-auth-provider";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { ClientLogout } from "@/modules/ui/components/client-logout";
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";
@@ -23,7 +25,7 @@ const AppLayout = async ({ children }: { children: React.ReactNode }) => {
}
return (
<>
<NextAuthProvider sessionMaxAge={SESSION_MAX_AGE}>
<NoMobileOverlay />
{POSTHOG_KEY && user && (
<PostHogIdentify posthogKey={POSTHOG_KEY} userId={user.id} email={user.email} name={user.name} />
@@ -39,7 +41,7 @@ const AppLayout = async ({ children }: { children: React.ReactNode }) => {
)}
<ToasterClient />
{children}
</>
</NextAuthProvider>
);
};
@@ -0,0 +1,21 @@
import type { TUserLocale } from "@formbricks/types/user";
import { getTranslate } from "@/lingodotdev/server";
interface NoScriptWarningProps {
locale: TUserLocale;
}
export const NoScriptWarning = async ({ locale }: NoScriptWarningProps) => {
const t = await getTranslate(locale);
return (
<noscript>
<div className="fixed inset-0 z-[9999] flex h-dvh w-full items-center justify-center bg-slate-50">
<div className="rounded-xl border border-slate-200 bg-white p-8 text-center shadow-lg">
<h1 className="mb-4 text-2xl font-bold text-slate-800">{t("common.javascript_required")}</h1>
<p className="text-slate-600">{t("common.javascript_required_description")}</p>
</div>
</div>
</noscript>
);
};
+2
View File
@@ -1,5 +1,6 @@
import { Metadata } from "next";
import React from "react";
import { NoScriptWarning } from "@/app/components/NoScriptWarning";
import { SentryProvider } from "@/app/sentry/SentryProvider";
import {
DEFAULT_LOCALE,
@@ -26,6 +27,7 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
return (
<html lang={locale} translate="no">
<body className="flex h-dvh flex-col transition-all ease-in-out">
<NoScriptWarning locale={locale} />
<SentryProvider
sentryDsn={SENTRY_DSN}
sentryRelease={SENTRY_RELEASE}
+16 -9
View File
@@ -140,6 +140,7 @@ checksums:
common/connect: 8778ee245078a8be4a2ce855c8c56edc
common/connect_formbricks: a9dd747575e7e035da69251366df6f95
common/connected: aa0ceca574641de34c74b9e590664230
common/contact: 9afa39bc47019ee6dec6c74b6273967c
common/contacts: d5b6c3f890b3904eaf5754081945c03d
common/continue: 3cfba90b4600131e82fc4260c568d044
common/copied: 29208e06d704c4fc4b8b534dc7acc4ef
@@ -187,12 +188,12 @@ checksums:
common/duplicate_copy_number: 083cfffd294672043dcbcc4c3dfeac6a
common/e_commerce: b9584e7d0449a6d1b0c182d7ff14061e
common/edit: eee7f39ff90b18852afc1671f21fbaa9
common/elements: 8cb054d952b341e5965284860d532bc7
common/email: e7f34943a0c2fb849db1839ff6ef5cb5
common/ending_card: 16d30d3a36472159da8c2dbd374dfe22
common/enter_url: 468c2276d0f2cb971ff5a47a20fa4b97
common/enterprise_license: e81bf506f47968870c7bd07245648a0d
common/environment: 0844e8dc1485339c8de066dc0a9bb6a1
common/environment_not_found: 4d7610bdb55a8b5e6131bb5b08ce04c5
common/environment_notice: 228a8668be1812e031f438d166861729
common/error: 3c95bcb32c2104b99a46f5b3dd015248
common/error_component_description: fa9eee04f864c3fe6e6681f716caa015
@@ -234,6 +235,8 @@ checksums:
common/invalid_file_type: f0c83e7d61dbad8250abb59869af4b9e
common/invite: 181884cea804cbde665f160811ee7ad0
common/invite_them: d4b7aadbd3c924b04ad4fce419709f10
common/javascript_required: d7988e5934af4d0df54fda369c0e4fb6
common/javascript_required_description: 4b65f456db79af4898888a3dd034fe2f
common/key: 3d1065ab98a1c2f1210507fd5c7bf515
common/label: a5c71bf158481233f8215dbd38cc196b
common/language: 277fd1a41cc237a437cd1d5e4a80463b
@@ -254,7 +257,9 @@ checksums:
common/marketing: fcf0f06f8b64b458c7ca6d95541a3cc8
common/members: 0932e80cba1e3e0a7f52bb67ff31da32
common/members_and_teams: bf5c3fadcb9fc23533ec1532b805ac08
common/membership: 83c856bbc2af99d8c3d860959d1d2a85
common/membership_not_found: 7ac63584af23396aace9992ad919ffd4
common/meta: 842eac888f134f3525f8ea613d933687
common/metadata: 695d4f7da261ba76e3be4de495491028
common/mobile_overlay_app_works_best_on_desktop: 4509f7bfbb4edbd42e534042d6cb7e72
common/mobile_overlay_surveys_look_good: d85169e86077738b9837647bf6d1c7d2
@@ -294,10 +299,9 @@ checksums:
common/or: 7b133c38bec0d5ee23cc6bcf9a8de50b
common/organization: 3dc8489af7e74121f65ce6d9677bc94d
common/organization_id: ef09b71c84a25b5da02a23c77e68a335
common/organization_not_found: 4cb8c07ec2c599b6f48750e06ffa182b
common/organization_settings: 11528aa89ae9935e55dcb54478058775
common/organization_teams_not_found: ce29fcb7a4e8b4582f92b65dea9b7d4e
common/other: 79acaa6cd481262bea4e743a422529d2
common/other_filters: 20b09213c131db47eb8b23e72d0c4bea
common/others: 39160224ce0e35eb4eb252c997edf4d8
common/overlay_color: 4b72073285d13fff93d094aabffe05ac
common/overview: 30c54e4dc4ce599b87d94be34a8617f5
@@ -389,7 +393,6 @@ checksums:
common/survey_id: 08303e98b3d4134947256e494b0c829e
common/survey_languages: 93e4a10ab190e6b1e1f7fe5f702df249
common/survey_live: d1f370505c67509e7b2759952daba20d
common/survey_not_found: 0485ea98d13a414eeefc8f1118b9c293
common/survey_paused: c770d174d6b57e8425a54906a09c8b39
common/survey_type: 417fcfecf8eaedefc4f11172426811f9
common/surveys: 33f68ad4111b32a6361beb9d5c184533
@@ -404,7 +407,6 @@ checksums:
common/team_name: 549d949de4b9adad4afd6427a60a329e
common/team_role: 66db395781aef64ef3791417b3b67c0b
common/teams: b63448c05270497973ac4407047dae02
common/teams_not_found: 02f333a64a83c1c014d8900ec9666345
common/text: 4ddccc1974775ed7357f9beaf9361cec
common/time: b504a03d52e8001bfdc5cb6205364f42
common/time_to_finish: c8f6abdb886bee3619bb50b08fada5fa
@@ -428,7 +430,6 @@ checksums:
common/url: ca97457614226960d41dd18c3c29c86b
common/user: 61073457a5c3901084b557d065f876be
common/user_id: 37f5ba37f71cb50607af32a6a203b1d4
common/user_not_found: 5903581136ac6c1c1351a482a6d8fdf7
common/variable: c13db5775ba9791b1522cc55c9c7acce
common/variable_ids: 44bf93b70703b7699fa9f21bc6c8eed4
common/variables: ffd3eec5497af36d7b4e4185bad1313a
@@ -444,14 +445,13 @@ checksums:
common/weeks: 545de30df4f44d3f6d1d344af6a10815
common/welcome_card: 76081ebd5b2e35da9b0f080323704ae7
common/workflows: b0c9c8615a9ba7d9cb73e767290a7f72
common/workspace: b63ef0e99ee6f7fef6cbe4971ca6cf0f
common/workspace_configuration: d0a5812d6a97d7724d565b1017c34387
common/workspace_created_successfully: bf401ae83da954f1db48724e2a8e40f1
common/workspace_creation_description: aea2f480ba0c54c5cabac72c9c900ddf
common/workspace_id: bafef925e1b57b52a69844fdf47aac3c
common/workspace_name: 14c04a902a874ab5ddbe9cf369ef0414
common/workspace_name_placeholder: 8a9e30ab01666af13c44a73b82c37ec1
common/workspace_not_found: 038fb0aaf3570610f4377b9eaed13752
common/workspace_permission_not_found: e94bdff8af51175c5767714f82bb4833
common/workspaces: 8ba082a84aa35cf851af1cf874b853e2
common/years: eb4f5fdd2b320bf13e200fd6a6c1abff
common/you: db2a4a796b70cc1430d1b21f6ffb6dcb
@@ -627,7 +627,6 @@ checksums:
environments/contacts/attributes_msg_new_attribute_created: 5cba6158c4305c05104814ec1479267c
environments/contacts/attributes_msg_userid_already_exists: 9c695538befc152806c460f52a73821a
environments/contacts/contact_deleted_successfully: c5b64a42a50e055f9e27ec49e20e03fa
environments/contacts/contact_not_found: 045396f0b13fafd43612a286263737c0
environments/contacts/contacts_table_refresh: 6a959475991dd4ab28ad881bae569a09
environments/contacts/contacts_table_refresh_success: 40951396e88e5c8fdafa0b3bb4fadca8
environments/contacts/create_attribute: 87320615901f95b4f35ee83c290a3a6c
@@ -807,8 +806,14 @@ checksums:
environments/integrations/webhooks/created_by_third_party: b40197eabbbce500b80b44268b8b1ee9
environments/integrations/webhooks/discord_webhook_not_supported: 23432534f908b2ba63a517fb1f9bbe0e
environments/integrations/webhooks/empty_webhook_message: 4c4d8709576a38cb8eb59866331d2405
environments/integrations/webhooks/endpoint_bad_gateway_error: 48ab17e9a77030b289ec22f497f50b63
environments/integrations/webhooks/endpoint_gateway_timeout_error: 5da45e2f6933927d1f8b0aaa9566e6a6
environments/integrations/webhooks/endpoint_internal_server_error: 6773fc34349febf95475cde88d8ee072
environments/integrations/webhooks/endpoint_method_not_allowed_error: 9963b503311393f4d7bffae9df46d422
environments/integrations/webhooks/endpoint_not_found_error: 607b75b7b7aa92ca81fe44e466f7c318
environments/integrations/webhooks/endpoint_pinged: 3b1fce00e61d4b9d2bdca390649c58b6
environments/integrations/webhooks/endpoint_pinged_error: 96c312fe8214757c4a934cdfbe177027
environments/integrations/webhooks/endpoint_service_unavailable_error: f9d4874c322f2963f5afaede354c9416
environments/integrations/webhooks/learn_to_verify: 25b2a035e2109170b28f4e16db76ad39
environments/integrations/webhooks/no_triggers: 6b68cddfc45b3f7e20644a24a1bbea69
environments/integrations/webhooks/please_check_console: 7b1787e82a0d762df02c011ebb1650ea
@@ -1606,6 +1611,8 @@ checksums:
environments/surveys/edit/response_limit_needs_to_exceed_number_of_received_responses: 9a9c223c0918ded716ddfaa84fbaa8d9
environments/surveys/edit/response_limits_redirections_and_more: e4f1cf94e56ad0e1b08701158d688802
environments/surveys/edit/response_options: 2988136d5248d7726583108992dcbaee
environments/surveys/edit/reverse_order_occasionally: 170fd50de940f382fa2e605228e4e088
environments/surveys/edit/reverse_order_occasionally_except_last: 1c833001b940f1419dd7534b199a0b4a
environments/surveys/edit/roundness: 5a161c8f5f258defb57ed1d551737cc4
environments/surveys/edit/roundness_description: 03940a6871ae43efa4810cba7cadb74b
environments/surveys/edit/row_used_in_logic_error: f89453ff1b6db77ad84af840fedd9813
+2 -2
View File
@@ -1,7 +1,7 @@
"use server";
import "server-only";
import { AuthorizationError } from "@formbricks/types/errors";
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getOrganizationByEnvironmentId } from "../../organization/service";
import { getMembershipByUserIdOrganizationId } from "../service";
@@ -9,7 +9,7 @@ export const getMembershipByUserIdOrganizationIdAction = async (environmentId: s
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!organization) {
throw new Error("Organization not found");
throw new ResourceNotFoundError("Organization", null);
}
const currentUserMembership = await getMembershipRole(userId, organization.id);
+1 -1
View File
@@ -378,7 +378,7 @@ export const getResponseDownloadFile = async (
const organizationId = await getOrganizationIdFromEnvironmentId(survey.environmentId);
if (!organizationId) {
throw new Error("Organization ID not found");
throw new ResourceNotFoundError("Organization", null);
}
const organizationBilling = await getOrganizationBilling(organizationId);
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "Verbinden",
"connect_formbricks": "Formbricks verbinden",
"connected": "Verbunden",
"contact": "Kontakt",
"contacts": "Kontakte",
"continue": "Weitermachen",
"copied": "Kopiert",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(Kopie {copyNumber})",
"e_commerce": "E-Commerce",
"edit": "Bearbeiten",
"elements": "Elemente",
"email": "E-Mail",
"ending_card": "Abschluss-Karte",
"enter_url": "URL eingeben",
"enterprise_license": "Enterprise Lizenz",
"environment": "Umgebung",
"environment_not_found": "Umgebung nicht gefunden",
"environment_notice": "Du befindest dich derzeit in der {environment}-Umgebung.",
"error": "Fehler",
"error_component_description": "Diese Ressource existiert nicht oder Du hast nicht die notwendigen Rechte, um darauf zuzugreifen.",
@@ -261,6 +262,8 @@
"invalid_file_type": "Ungültiger Dateityp",
"invite": "Einladen",
"invite_them": "Lade sie ein",
"javascript_required": "JavaScript erforderlich",
"javascript_required_description": "Formbricks benötigt JavaScript, um ordnungsgemäß zu funktionieren. Bitte aktiviere JavaScript in deinen Browsereinstellungen, um fortzufahren.",
"key": "Schlüssel",
"label": "Bezeichnung",
"language": "Sprache",
@@ -281,7 +284,9 @@
"marketing": "Marketing",
"members": "Mitglieder",
"members_and_teams": "Mitglieder & Teams",
"membership": "Mitgliedschaft",
"membership_not_found": "Mitgliedschaft nicht gefunden",
"meta": "Meta",
"metadata": "Metadaten",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funktioniert am besten auf einem größeren Bildschirm. Um Umfragen zu verwalten oder zu erstellen, wechsle zu einem anderen Gerät.",
"mobile_overlay_surveys_look_good": "Keine Sorge deine Umfragen sehen auf jedem Gerät und jeder Bildschirmgröße großartig aus!",
@@ -321,10 +326,9 @@
"or": "oder",
"organization": "Organisation",
"organization_id": "Organisations-ID",
"organization_not_found": "Organisation nicht gefunden",
"organization_settings": "Organisationseinstellungen",
"organization_teams_not_found": "Organisations-Teams nicht gefunden",
"other": "Andere",
"other_filters": "Weitere Filter",
"others": "Andere",
"overlay_color": "Overlay-Farbe",
"overview": "Überblick",
@@ -416,7 +420,6 @@
"survey_id": "Umfrage-ID",
"survey_languages": "Umfragesprachen",
"survey_live": "Umfrage live",
"survey_not_found": "Umfrage nicht gefunden",
"survey_paused": "Umfrage pausiert.",
"survey_type": "Umfragetyp",
"surveys": "Umfragen",
@@ -431,7 +434,6 @@
"team_name": "Teamname",
"team_role": "Team-Rolle",
"teams": "Teams",
"teams_not_found": "Teams nicht gefunden",
"text": "Text",
"time": "Zeit",
"time_to_finish": "Zeit zum Fertigstellen",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "Benutzer",
"user_id": "Benutzer-ID",
"user_not_found": "Benutzer nicht gefunden",
"variable": "Variable",
"variable_ids": "Variablen-IDs",
"variables": "Variablen",
@@ -471,14 +472,13 @@
"weeks": "Wochen",
"welcome_card": "Willkommenskarte",
"workflows": "Workflows",
"workspace": "Arbeitsbereich",
"workspace_configuration": "Projektkonfiguration",
"workspace_created_successfully": "Projekt erfolgreich erstellt",
"workspace_creation_description": "Organisieren Sie Umfragen in Projekten für eine bessere Zugriffskontrolle.",
"workspace_id": "Projekt-ID",
"workspace_name": "Projektname",
"workspace_name_placeholder": "z. B. Formbricks",
"workspace_not_found": "Projekt nicht gefunden",
"workspace_permission_not_found": "Projektberechtigung nicht gefunden",
"workspaces": "Projekte",
"years": "Jahre",
"you": "Du",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "Neues Attribut “{key}” mit Typ “{dataType}” erstellt",
"attributes_msg_userid_already_exists": "Die Benutzer-ID existiert bereits für diese Umgebung und wurde nicht aktualisiert.",
"contact_deleted_successfully": "Kontakt erfolgreich gelöscht",
"contact_not_found": "Kein solcher Kontakt gefunden",
"contacts_table_refresh": "Kontakte aktualisieren",
"contacts_table_refresh_success": "Kontakte erfolgreich aktualisiert",
"create_attribute": "Attribut erstellen",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "Connect",
"connect_formbricks": "Connect Formbricks",
"connected": "Connected",
"contact": "Contact",
"contacts": "Contacts",
"continue": "Continue",
"copied": "Copied",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(copy {copyNumber})",
"e_commerce": "E-Commerce",
"edit": "Edit",
"elements": "Elements",
"email": "Email",
"ending_card": "Ending card",
"enter_url": "Enter URL",
"enterprise_license": "Enterprise License",
"environment": "Environment",
"environment_not_found": "Environment not found",
"environment_notice": "You are currently in the {environment} environment.",
"error": "Error",
"error_component_description": "This resource does not exist or you do not have the necessary rights to access it.",
@@ -261,6 +262,8 @@
"invalid_file_type": "Invalid file type",
"invite": "Invite",
"invite_them": "Invite them",
"javascript_required": "JavaScript Required",
"javascript_required_description": "Formbricks requires JavaScript to function properly. Please enable JavaScript in your browser settings to continue.",
"key": "Key",
"label": "Label",
"language": "Language",
@@ -281,7 +284,9 @@
"marketing": "Marketing",
"members": "Members",
"members_and_teams": "Members & Teams",
"membership": "Membership",
"membership_not_found": "Membership not found",
"meta": "Meta",
"metadata": "Metadata",
"mobile_overlay_app_works_best_on_desktop": "Formbricks works best on a bigger screen. To manage or build surveys, switch to another device.",
"mobile_overlay_surveys_look_good": "Do not worry your surveys look great on every device and screen size!",
@@ -321,10 +326,9 @@
"or": "or",
"organization": "Organization",
"organization_id": "Organization ID",
"organization_not_found": "Organization not found",
"organization_settings": "Organization settings",
"organization_teams_not_found": "Organization teams not found",
"other": "Other",
"other_filters": "Other Filters",
"others": "Others",
"overlay_color": "Overlay color",
"overview": "Overview",
@@ -416,7 +420,6 @@
"survey_id": "Survey ID",
"survey_languages": "Survey Languages",
"survey_live": "Survey live",
"survey_not_found": "Survey not found",
"survey_paused": "Survey paused.",
"survey_type": "Survey Type",
"surveys": "Surveys",
@@ -431,7 +434,6 @@
"team_name": "Team name",
"team_role": "Team role",
"teams": "Teams",
"teams_not_found": "Teams not found",
"text": "Text",
"time": "Time",
"time_to_finish": "Time to finish",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "User",
"user_id": "User ID",
"user_not_found": "User not found",
"variable": "Variable",
"variable_ids": "Variable IDs",
"variables": "Variables",
@@ -471,14 +472,13 @@
"weeks": "weeks",
"welcome_card": "Welcome card",
"workflows": "Workflows",
"workspace": "Workspace",
"workspace_configuration": "Workspace Configuration",
"workspace_created_successfully": "Workspace created successfully",
"workspace_creation_description": "Organize surveys in workspaces for better access control.",
"workspace_id": "Workspace ID",
"workspace_name": "Workspace Name",
"workspace_name_placeholder": "e.g. Formbricks",
"workspace_not_found": "Workspace not found",
"workspace_permission_not_found": "Workspace permission not found",
"workspaces": "Workspaces",
"years": "years",
"you": "You",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "Created new attribute “{key}” with type “{dataType}”",
"attributes_msg_userid_already_exists": "The user ID already exists for this environment and was not updated.",
"contact_deleted_successfully": "Contact deleted successfully",
"contact_not_found": "No such contact found",
"contacts_table_refresh": "Refresh contacts",
"contacts_table_refresh_success": "Contacts refreshed successfully",
"create_attribute": "Create attribute",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "Conectar",
"connect_formbricks": "Conectar Formbricks",
"connected": "Conectado",
"contact": "Contacto",
"contacts": "Contactos",
"continue": "Continuar",
"copied": "Copiado",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(copia {copyNumber})",
"e_commerce": "Comercio electrónico",
"edit": "Editar",
"elements": "Elementos",
"email": "Email",
"ending_card": "Tarjeta final",
"enter_url": "Introducir URL",
"enterprise_license": "Licencia empresarial",
"environment": "Entorno",
"environment_not_found": "Entorno no encontrado",
"environment_notice": "Actualmente estás en el entorno {environment}.",
"error": "Error",
"error_component_description": "Este recurso no existe o no tienes los derechos necesarios para acceder a él.",
@@ -261,6 +262,8 @@
"invalid_file_type": "Tipo de archivo no válido",
"invite": "Invitar",
"invite_them": "Invítales",
"javascript_required": "Se requiere JavaScript",
"javascript_required_description": "Formbricks requiere JavaScript para funcionar correctamente. Por favor, activa JavaScript en la configuración de tu navegador para continuar.",
"key": "Clave",
"label": "Etiqueta",
"language": "Idioma",
@@ -281,7 +284,9 @@
"marketing": "Marketing",
"members": "Miembros",
"members_and_teams": "Miembros y equipos",
"membership": "Membresía",
"membership_not_found": "Membresía no encontrada",
"meta": "Meta",
"metadata": "Metadatos",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona mejor en una pantalla más grande. Para gestionar o crear encuestas, cambia a otro dispositivo.",
"mobile_overlay_surveys_look_good": "No te preocupes ¡tus encuestas se ven geniales en todos los dispositivos y tamaños de pantalla!",
@@ -321,10 +326,9 @@
"or": "o",
"organization": "Organización",
"organization_id": "ID de organización",
"organization_not_found": "Organización no encontrada",
"organization_settings": "Ajustes de la organización",
"organization_teams_not_found": "Equipos de la organización no encontrados",
"other": "Otro",
"other_filters": "Otros Filtros",
"others": "Otros",
"overlay_color": "Color de superposición",
"overview": "Resumen",
@@ -416,7 +420,6 @@
"survey_id": "ID de encuesta",
"survey_languages": "Idiomas de la encuesta",
"survey_live": "Encuesta activa",
"survey_not_found": "Encuesta no encontrada",
"survey_paused": "Encuesta pausada.",
"survey_type": "Tipo de encuesta",
"surveys": "Encuestas",
@@ -431,7 +434,6 @@
"team_name": "Nombre del equipo",
"team_role": "Rol del equipo",
"teams": "Equipos",
"teams_not_found": "Equipos no encontrados",
"text": "Texto",
"time": "Hora",
"time_to_finish": "Tiempo para finalizar",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "Usuario",
"user_id": "ID de usuario",
"user_not_found": "Usuario no encontrado",
"variable": "Variable",
"variable_ids": "IDs de variables",
"variables": "Variables",
@@ -471,14 +472,13 @@
"weeks": "semanas",
"welcome_card": "Tarjeta de bienvenida",
"workflows": "Flujos de trabajo",
"workspace": "Espacio de trabajo",
"workspace_configuration": "Configuración del proyecto",
"workspace_created_successfully": "Proyecto creado correctamente",
"workspace_creation_description": "Organiza las encuestas en proyectos para un mejor control de acceso.",
"workspace_id": "ID del proyecto",
"workspace_name": "Nombre del proyecto",
"workspace_name_placeholder": "p. ej. Formbricks",
"workspace_not_found": "Proyecto no encontrado",
"workspace_permission_not_found": "Permiso del proyecto no encontrado",
"workspaces": "Proyectos",
"years": "años",
"you": "Tú",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "Se creó el atributo nuevo “{key}” con el tipo “{dataType}”",
"attributes_msg_userid_already_exists": "El ID de usuario ya existe para este entorno y no se actualizó.",
"contact_deleted_successfully": "Contacto eliminado correctamente",
"contact_not_found": "No se ha encontrado dicho contacto",
"contacts_table_refresh": "Actualizar contactos",
"contacts_table_refresh_success": "Contactos actualizados correctamente",
"create_attribute": "Crear atributo",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "Connecter",
"connect_formbricks": "Connecter Formbricks",
"connected": "Connecté",
"contact": "Contact",
"contacts": "Contacts",
"continue": "Continuer",
"copied": "Copié",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(copie {copyNumber})",
"e_commerce": "E-commerce",
"edit": "Modifier",
"elements": "Éléments",
"email": "Email",
"ending_card": "Carte de fin",
"enter_url": "Saisir l'URL",
"enterprise_license": "Licence d'entreprise",
"environment": "Environnement",
"environment_not_found": "Environnement non trouvé",
"environment_notice": "Vous êtes actuellement dans l'environnement {environment}.",
"error": "Erreur",
"error_component_description": "Cette ressource n'existe pas ou vous n'avez pas les droits nécessaires pour y accéder.",
@@ -261,6 +262,8 @@
"invalid_file_type": "Type de fichier invalide",
"invite": "Inviter",
"invite_them": "Invitez-les",
"javascript_required": "JavaScript requis",
"javascript_required_description": "Formbricks nécessite JavaScript pour fonctionner correctement. Veuillez activer JavaScript dans les paramètres de votre navigateur pour continuer.",
"key": "Clé",
"label": "Étiquette",
"language": "Langue",
@@ -281,7 +284,9 @@
"marketing": "Marketing",
"members": "Membres",
"members_and_teams": "Membres & Équipes",
"membership": "Adhésion",
"membership_not_found": "Abonnement non trouvé",
"meta": "Méta",
"metadata": "Métadonnées",
"mobile_overlay_app_works_best_on_desktop": "Formbricks fonctionne mieux sur un écran plus grand. Pour gérer ou créer des sondages, passez à un autre appareil.",
"mobile_overlay_surveys_look_good": "Ne t'inquiète pas tes enquêtes sont superbes sur tous les appareils et tailles d'écran!",
@@ -321,10 +326,9 @@
"or": "ou",
"organization": "Organisation",
"organization_id": "Identifiant de l'organisation",
"organization_not_found": "Organisation non trouvée",
"organization_settings": "Paramètres de l'organisation",
"organization_teams_not_found": "Équipes d'organisation non trouvées",
"other": "Autre",
"other_filters": "Autres filtres",
"others": "Autres",
"overlay_color": "Couleur de superposition",
"overview": "Aperçu",
@@ -416,7 +420,6 @@
"survey_id": "ID de l'enquête",
"survey_languages": "Langues de l'enquête",
"survey_live": "Sondage en direct",
"survey_not_found": "Sondage non trouvé",
"survey_paused": "Sondage en pause.",
"survey_type": "Type de sondage",
"surveys": "Enquêtes",
@@ -431,7 +434,6 @@
"team_name": "Nom de l'équipe",
"team_role": "Rôle dans l'équipe",
"teams": "Équipes",
"teams_not_found": "Équipes non trouvées",
"text": "Texte",
"time": "Temps",
"time_to_finish": "Temps de finir",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "Utilisateur",
"user_id": "Identifiant d'utilisateur",
"user_not_found": "Utilisateur non trouvé",
"variable": "Variable",
"variable_ids": "Identifiants variables",
"variables": "Variables",
@@ -471,14 +472,13 @@
"weeks": "semaines",
"welcome_card": "Carte de bienvenue",
"workflows": "Workflows",
"workspace": "Espace de travail",
"workspace_configuration": "Configuration du projet",
"workspace_created_successfully": "Projet créé avec succès",
"workspace_creation_description": "Organisez les enquêtes dans des projets pour un meilleur contrôle d'accès.",
"workspace_id": "ID du projet",
"workspace_name": "Nom du projet",
"workspace_name_placeholder": "par ex. Formbricks",
"workspace_not_found": "Projet introuvable",
"workspace_permission_not_found": "Permission du projet introuvable",
"workspaces": "Projets",
"years": "années",
"you": "Vous",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "Nouvel attribut “{key}” créé avec le type “{dataType}”",
"attributes_msg_userid_already_exists": "L'identifiant utilisateur existe déjà pour cet environnement et n'a pas été mis à jour.",
"contact_deleted_successfully": "Contact supprimé avec succès",
"contact_not_found": "Aucun contact trouvé",
"contacts_table_refresh": "Actualiser les contacts",
"contacts_table_refresh_success": "Contacts rafraîchis avec succès",
"create_attribute": "Créer un attribut",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "Kapcsolódás",
"connect_formbricks": "Kapcsolódás a Formbrickshez",
"connected": "Kapcsolódva",
"contact": "Kapcsolat",
"contacts": "Partnerek",
"continue": "Folytatás",
"copied": "Másolva",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "({copyNumber}. másolat)",
"e_commerce": "E-kereskedelem",
"edit": "Szerkesztés",
"elements": "Elemek",
"email": "E-mail",
"ending_card": "Befejező kártya",
"enter_url": "URL megadása",
"enterprise_license": "Vállalati licenc",
"environment": "Környezet",
"environment_not_found": "A környezet nem található",
"environment_notice": "Ön jelenleg a(z) {environment} környezetben van.",
"error": "Hiba",
"error_component_description": "Ez az erőforrás nem létezik, vagy nem rendelkezik a hozzáféréshez szükséges jogosultságokkal.",
@@ -261,6 +262,8 @@
"invalid_file_type": "Érvénytelen fájltípus",
"invite": "Meghívás",
"invite_them": "Meghívó nekik",
"javascript_required": "JavaScript szükséges",
"javascript_required_description": "A Formbricks használatához JavaScript szükséges. Kérjük, engedélyezze a JavaScriptet a böngésző beállításaiban a folytatáshoz.",
"key": "Kulcs",
"label": "Címke",
"language": "Nyelv",
@@ -281,7 +284,9 @@
"marketing": "Marketing",
"members": "Tagok",
"members_and_teams": "Tagok és csapatok",
"membership": "Tagság",
"membership_not_found": "A tagság nem található",
"meta": "Meta",
"metadata": "Metaadatok",
"mobile_overlay_app_works_best_on_desktop": "A Formbricks nagyobb képernyőn működik a legjobban. A kérdőívek kezeléséhez vagy összeállításához váltson másik eszközre.",
"mobile_overlay_surveys_look_good": "Ne aggódjon a kérdőívei minden eszközön és képernyőméretnél remekül néznek ki!",
@@ -321,10 +326,9 @@
"or": "vagy",
"organization": "Szervezet",
"organization_id": "Szervezetazonosító",
"organization_not_found": "A szervezet nem található",
"organization_settings": "Szervezet beállításai",
"organization_teams_not_found": "A szervezeti csapatok nem találhatók",
"other": "Egyéb",
"other_filters": "Egyéb szűrők",
"others": "Mások",
"overlay_color": "Rávetítés színe",
"overview": "Áttekintés",
@@ -416,7 +420,6 @@
"survey_id": "Kérdőív-azonosító",
"survey_languages": "Kérdőív nyelvei",
"survey_live": "A kérdőív élő",
"survey_not_found": "A kérdőív nem található",
"survey_paused": "A kérdőív szüneteltetve.",
"survey_type": "Kérdőív típusa",
"surveys": "Kérdőívek",
@@ -431,7 +434,6 @@
"team_name": "Csapat neve",
"team_role": "Csapatszerep",
"teams": "Csapatok",
"teams_not_found": "A csapatok nem találhatók",
"text": "Szöveg",
"time": "Idő",
"time_to_finish": "Idő a befejezésig",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "Felhasználó",
"user_id": "Felhasználó-azonosító",
"user_not_found": "A felhasználó nem található",
"variable": "Változó",
"variable_ids": "Változóazonosítók",
"variables": "Változók",
@@ -471,14 +472,13 @@
"weeks": "hét",
"welcome_card": "Üdvözlő kártya",
"workflows": "Munkafolyamatok",
"workspace": "Munkaterület",
"workspace_configuration": "Munkaterület beállítása",
"workspace_created_successfully": "A munkaterület sikeresen létrehozva",
"workspace_creation_description": "Kérdőívek munkaterületekre szervezése a jobb hozzáférés-vezérlés érdekében.",
"workspace_id": "Munkaterület-azonosító",
"workspace_name": "Munkaterület neve",
"workspace_name_placeholder": "például Formbricks",
"workspace_not_found": "A munkaterület nem található",
"workspace_permission_not_found": "A munkaterület-jogosultság nem található",
"workspaces": "Munkaterületek",
"years": "év",
"you": "Ön",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "Az új „{dataType}” típusú „{key}” attribútum létrehozva",
"attributes_msg_userid_already_exists": "A felhasználó-azonosító már létezik ennél a környezetnél, és nem lett frissítve.",
"contact_deleted_successfully": "A partner sikeresen törölve",
"contact_not_found": "Nem található ilyen partner",
"contacts_table_refresh": "Partnerek frissítése",
"contacts_table_refresh_success": "A partnerek sikeresen frissítve",
"create_attribute": "Attribútum létrehozása",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "接続",
"connect_formbricks": "Formbricksを接続",
"connected": "接続済み",
"contact": "連絡先",
"contacts": "連絡先",
"continue": "続行",
"copied": "コピーしました",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(コピー {copyNumber})",
"e_commerce": "Eコマース",
"edit": "編集",
"elements": "要素",
"email": "メールアドレス",
"ending_card": "終了カード",
"enter_url": "URLを入力",
"enterprise_license": "エンタープライズライセンス",
"environment": "環境",
"environment_not_found": "環境が見つかりません",
"environment_notice": "現在、{environment} 環境にいます。",
"error": "エラー",
"error_component_description": "この リソース は 存在 しない か、アクセス する ための 必要な 権限 が ありません。",
@@ -261,6 +262,8 @@
"invalid_file_type": "無効なファイルタイプです",
"invite": "招待",
"invite_them": "招待する",
"javascript_required": "JavaScriptが必要です",
"javascript_required_description": "Formbricksを正常に動作させるには、JavaScriptが必要です。続行するには、ブラウザの設定でJavaScriptを有効にしてください。",
"key": "キー",
"label": "ラベル",
"language": "言語",
@@ -281,7 +284,9 @@
"marketing": "マーケティング",
"members": "メンバー",
"members_and_teams": "メンバー&チーム",
"membership": "メンバーシップ",
"membership_not_found": "メンバーシップが見つかりません",
"meta": "メタ",
"metadata": "メタデータ",
"mobile_overlay_app_works_best_on_desktop": "Formbricks は より 大きな 画面 で最適に 作動します。 フォーム を 管理または 構築する には、 別の デバイス に 切り替える 必要が あります。",
"mobile_overlay_surveys_look_good": "ご安心ください - お使い の デバイス や 画面 サイズ に 関係なく、 フォーム は 素晴らしく 見えます!",
@@ -321,10 +326,9 @@
"or": "または",
"organization": "組織",
"organization_id": "組織ID",
"organization_not_found": "組織が見つかりません",
"organization_settings": "組織設定",
"organization_teams_not_found": "組織のチームが見つかりません",
"other": "その他",
"other_filters": "その他のフィルター",
"others": "その他",
"overlay_color": "オーバーレイの色",
"overview": "概要",
@@ -416,7 +420,6 @@
"survey_id": "フォームID",
"survey_languages": "フォームの言語",
"survey_live": "フォーム公開中",
"survey_not_found": "フォームが見つかりません",
"survey_paused": "フォームは一時停止中です。",
"survey_type": "フォームの種類",
"surveys": "フォーム",
@@ -431,7 +434,6 @@
"team_name": "チーム名",
"team_role": "チームの役割",
"teams": "チーム",
"teams_not_found": "チームが見つかりません",
"text": "テキスト",
"time": "時間",
"time_to_finish": "所要時間",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "ユーザー",
"user_id": "ユーザーID",
"user_not_found": "ユーザーが見つかりません",
"variable": "変数",
"variable_ids": "変数ID",
"variables": "変数",
@@ -471,14 +472,13 @@
"weeks": "週間",
"welcome_card": "ウェルカムカード",
"workflows": "ワークフロー",
"workspace": "ワークスペース",
"workspace_configuration": "ワークスペース設定",
"workspace_created_successfully": "ワークスペースが正常に作成されました",
"workspace_creation_description": "アクセス制御を改善するために、フォームをワークスペースで整理します。",
"workspace_id": "ワークスペースID",
"workspace_name": "ワークスペース名",
"workspace_name_placeholder": "例: Formbricks",
"workspace_not_found": "ワークスペースが見つかりません",
"workspace_permission_not_found": "ワークスペースの権限が見つかりません",
"workspaces": "ワークスペース",
"years": "年",
"you": "あなた",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "新しい属性“{key}”を型“{dataType}”で作成しました",
"attributes_msg_userid_already_exists": "この環境にはすでにユーザーIDが存在するため、更新されませんでした。",
"contact_deleted_successfully": "連絡先を正常に削除しました",
"contact_not_found": "そのような連絡先は見つかりません",
"contacts_table_refresh": "連絡先を更新",
"contacts_table_refresh_success": "連絡先を正常に更新しました",
"create_attribute": "属性を作成",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "Verbinden",
"connect_formbricks": "Sluit Formbricks aan",
"connected": "Aangesloten",
"contact": "Contact",
"contacts": "Contacten",
"continue": "Doorgaan",
"copied": "Gekopieerd",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(kopie {copyNumber})",
"e_commerce": "E-commerce",
"edit": "Bewerking",
"elements": "Elementen",
"email": "E-mail",
"ending_card": "Einde kaart",
"enter_url": "URL invoeren",
"enterprise_license": "Enterprise-licentie",
"environment": "Omgeving",
"environment_not_found": "Omgeving niet gevonden",
"environment_notice": "U bevindt zich momenteel in de {environment}-omgeving.",
"error": "Fout",
"error_component_description": "Deze bron bestaat niet of u beschikt niet over de benodigde toegangsrechten.",
@@ -261,6 +262,8 @@
"invalid_file_type": "Ongeldig bestandstype",
"invite": "Uitnodiging",
"invite_them": "Nodig ze uit",
"javascript_required": "JavaScript vereist",
"javascript_required_description": "Formbricks heeft JavaScript nodig om correct te functioneren. Schakel JavaScript in je browserinstellingen in om door te gaan.",
"key": "Sleutel",
"label": "Label",
"language": "Taal",
@@ -281,7 +284,9 @@
"marketing": "Marketing",
"members": "Leden",
"members_and_teams": "Leden & teams",
"membership": "Lidmaatschap",
"membership_not_found": "Lidmaatschap niet gevonden",
"meta": "Meta",
"metadata": "Metagegevens",
"mobile_overlay_app_works_best_on_desktop": "Formbricks werkt het beste op een groter scherm. Schakel over naar een ander apparaat om enquêtes te beheren of samen te stellen.",
"mobile_overlay_surveys_look_good": "Maakt u zich geen zorgen: uw enquêtes zien er geweldig uit op elk apparaat en schermformaat!",
@@ -321,10 +326,9 @@
"or": "of",
"organization": "Organisatie",
"organization_id": "Organisatie-ID",
"organization_not_found": "Organisatie niet gevonden",
"organization_settings": "Organisatie-instellingen",
"organization_teams_not_found": "Organisatieteams niet gevonden",
"other": "Ander",
"other_filters": "Overige filters",
"others": "Anderen",
"overlay_color": "Overlaykleur",
"overview": "Overzicht",
@@ -416,7 +420,6 @@
"survey_id": "Enquête-ID",
"survey_languages": "Enquêtetalen",
"survey_live": "Enquête live",
"survey_not_found": "Enquête niet gevonden",
"survey_paused": "Enquête onderbroken.",
"survey_type": "Enquêtetype",
"surveys": "Enquêtes",
@@ -431,7 +434,6 @@
"team_name": "Teamnaam",
"team_role": "Teamrol",
"teams": "Teams",
"teams_not_found": "Teams niet gevonden",
"text": "Tekst",
"time": "Tijd",
"time_to_finish": "Tijd om af te ronden",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "Gebruiker",
"user_id": "Gebruikers-ID",
"user_not_found": "Gebruiker niet gevonden",
"variable": "Variabel",
"variable_ids": "Variabele ID's",
"variables": "Variabelen",
@@ -471,14 +472,13 @@
"weeks": "weken",
"welcome_card": "Welkomstkaart",
"workflows": "Workflows",
"workspace": "Werkruimte",
"workspace_configuration": "Werkruimte-configuratie",
"workspace_created_successfully": "Project succesvol aangemaakt",
"workspace_creation_description": "Organiseer enquêtes in werkruimtes voor beter toegangsbeheer.",
"workspace_id": "Werkruimte-ID",
"workspace_name": "Werkruimtenaam",
"workspace_name_placeholder": "bijv. Formbricks",
"workspace_not_found": "Werkruimte niet gevonden",
"workspace_permission_not_found": "Werkruimte-machtiging niet gevonden",
"workspaces": "Werkruimtes",
"years": "jaren",
"you": "Jij",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "Nieuw attribuut “{key}” aangemaakt met type “{dataType}”",
"attributes_msg_userid_already_exists": "De gebruikers-ID bestaat al voor deze omgeving en is niet bijgewerkt.",
"contact_deleted_successfully": "Contact succesvol verwijderd",
"contact_not_found": "Er is geen dergelijk contact gevonden",
"contacts_table_refresh": "Vernieuw contacten",
"contacts_table_refresh_success": "Contacten zijn vernieuwd",
"create_attribute": "Attribuut aanmaken",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "Conectar",
"connect_formbricks": "Conectar Formbricks",
"connected": "conectado",
"contact": "Contato",
"contacts": "Contatos",
"continue": "Continuar",
"copied": "Copiado",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(cópia {copyNumber})",
"e_commerce": "comércio eletrônico",
"edit": "Editar",
"elements": "Elementos",
"email": "Email",
"ending_card": "Cartão de encerramento",
"enter_url": "Inserir URL",
"enterprise_license": "Licença Empresarial",
"environment": "Ambiente",
"environment_not_found": "Ambiente não encontrado",
"environment_notice": "Você está atualmente no ambiente {environment}.",
"error": "Erro",
"error_component_description": "Esse recurso não existe ou você não tem permissão para acessá-lo.",
@@ -261,6 +262,8 @@
"invalid_file_type": "Tipo de arquivo inválido",
"invite": "convidar",
"invite_them": "Convida eles",
"javascript_required": "JavaScript Necessário",
"javascript_required_description": "O Formbricks precisa do JavaScript para funcionar corretamente. Por favor, ative o JavaScript nas configurações do seu navegador para continuar.",
"key": "Chave",
"label": "Etiqueta",
"language": "Língua",
@@ -281,7 +284,9 @@
"marketing": "marketing",
"members": "Membros",
"members_and_teams": "Membros e equipes",
"membership": "Associação",
"membership_not_found": "Assinatura não encontrada",
"meta": "Meta",
"metadata": "metadados",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona melhor em uma tela maior. Para gerenciar ou criar pesquisas, mude para outro dispositivo.",
"mobile_overlay_surveys_look_good": "Não se preocupe suas pesquisas ficam ótimas em qualquer dispositivo e tamanho de tela!",
@@ -321,10 +326,9 @@
"or": "ou",
"organization": "organização",
"organization_id": "ID da Organização",
"organization_not_found": "Organização não encontrada",
"organization_settings": "Configurações da Organização",
"organization_teams_not_found": "Equipes da organização não encontradas",
"other": "outro",
"other_filters": "Outros Filtros",
"others": "Outros",
"overlay_color": "Cor da sobreposição",
"overview": "Visão Geral",
@@ -416,7 +420,6 @@
"survey_id": "ID da Pesquisa",
"survey_languages": "Idiomas da Pesquisa",
"survey_live": "Pesquisa ao vivo",
"survey_not_found": "Pesquisa não encontrada",
"survey_paused": "Pesquisa pausada.",
"survey_type": "Tipo de Pesquisa",
"surveys": "Pesquisas",
@@ -431,7 +434,6 @@
"team_name": "Nome da equipe",
"team_role": "Função na equipe",
"teams": "Equipes",
"teams_not_found": "Equipes não encontradas",
"text": "Texto",
"time": "tempo",
"time_to_finish": "Hora de terminar",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "Usuário",
"user_id": "ID do usuário",
"user_not_found": "Usuário não encontrado",
"variable": "variável",
"variable_ids": "IDs de variáveis",
"variables": "Variáveis",
@@ -471,14 +472,13 @@
"weeks": "semanas",
"welcome_card": "Cartão de boas-vindas",
"workflows": "Fluxos de trabalho",
"workspace": "Espaço de trabalho",
"workspace_configuration": "Configuração do projeto",
"workspace_created_successfully": "Projeto criado com sucesso",
"workspace_creation_description": "Organize pesquisas em projetos para melhor controle de acesso.",
"workspace_id": "ID do projeto",
"workspace_name": "Nome do projeto",
"workspace_name_placeholder": "ex: Formbricks",
"workspace_not_found": "Projeto não encontrado",
"workspace_permission_not_found": "Permissão do projeto não encontrada",
"workspaces": "Projetos",
"years": "anos",
"you": "Você",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "Novo atributo “{key}” criado com tipo “{dataType}”",
"attributes_msg_userid_already_exists": "O ID de usuário já existe para este ambiente e não foi atualizado.",
"contact_deleted_successfully": "Contato excluído com sucesso",
"contact_not_found": "Nenhum contato encontrado",
"contacts_table_refresh": "Atualizar contatos",
"contacts_table_refresh_success": "Contatos atualizados com sucesso",
"create_attribute": "Criar atributo",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "Conectar",
"connect_formbricks": "Ligar Formbricks",
"connected": "Conectado",
"contact": "Contacto",
"contacts": "Contactos",
"continue": "Continuar",
"copied": "Copiado",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(cópia {copyNumber})",
"e_commerce": "Comércio Eletrónico",
"edit": "Editar",
"elements": "Elementos",
"email": "Email",
"ending_card": "Cartão de encerramento",
"enter_url": "Introduzir URL",
"enterprise_license": "Licença Enterprise",
"environment": "Ambiente",
"environment_not_found": "Ambiente não encontrado",
"environment_notice": "Está atualmente no ambiente {environment}.",
"error": "Erro",
"error_component_description": "Este recurso não existe ou não tem os direitos necessários para aceder a ele.",
@@ -261,6 +262,8 @@
"invalid_file_type": "Tipo de ficheiro inválido",
"invite": "Convidar",
"invite_them": "Convide-os",
"javascript_required": "JavaScript Necessário",
"javascript_required_description": "O Formbricks necessita de JavaScript para funcionar corretamente. Por favor, ativa o JavaScript nas definições do teu navegador para continuar.",
"key": "Chave",
"label": "Etiqueta",
"language": "Idioma",
@@ -281,7 +284,9 @@
"marketing": "Marketing",
"members": "Membros",
"members_and_teams": "Membros e equipas",
"membership": "Subscrição",
"membership_not_found": "Associação não encontrada",
"meta": "Meta",
"metadata": "Metadados",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona melhor num ecrã maior. Para gerir ou criar inquéritos, mude de dispositivo.",
"mobile_overlay_surveys_look_good": "Não se preocupe os seus inquéritos têm uma ótima aparência em todos os dispositivos e tamanhos de ecrã!",
@@ -321,10 +326,9 @@
"or": "ou",
"organization": "Organização",
"organization_id": "ID da Organização",
"organization_not_found": "Organização não encontrada",
"organization_settings": "Configurações da Organização",
"organization_teams_not_found": "Equipas da organização não encontradas",
"other": "Outro",
"other_filters": "Outros Filtros",
"others": "Outros",
"overlay_color": "Cor da sobreposição",
"overview": "Visão geral",
@@ -416,7 +420,6 @@
"survey_id": "ID do Inquérito",
"survey_languages": "Idiomas da Pesquisa",
"survey_live": "Inquérito ao vivo",
"survey_not_found": "Inquérito não encontrado",
"survey_paused": "Inquérito pausado.",
"survey_type": "Tipo de Inquérito",
"surveys": "Inquéritos",
@@ -431,7 +434,6 @@
"team_name": "Nome da equipa",
"team_role": "Função na equipa",
"teams": "Equipas",
"teams_not_found": "Equipas não encontradas",
"text": "Texto",
"time": "Tempo",
"time_to_finish": "Tempo para concluir",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "Utilizador",
"user_id": "ID do Utilizador",
"user_not_found": "Utilizador não encontrado",
"variable": "Variável",
"variable_ids": "IDs de variáveis",
"variables": "Variáveis",
@@ -471,14 +472,13 @@
"weeks": "semanas",
"welcome_card": "Cartão de boas-vindas",
"workflows": "Fluxos de trabalho",
"workspace": "Espaço de trabalho",
"workspace_configuration": "Configuração do projeto",
"workspace_created_successfully": "Projeto criado com sucesso",
"workspace_creation_description": "Organize inquéritos em projetos para melhor controlo de acesso.",
"workspace_id": "ID do projeto",
"workspace_name": "Nome do projeto",
"workspace_name_placeholder": "ex. Formbricks",
"workspace_not_found": "Projeto não encontrado",
"workspace_permission_not_found": "Permissão do projeto não encontrada",
"workspaces": "Projetos",
"years": "anos",
"you": "Você",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "Criado novo atributo “{key}” com tipo “{dataType}”",
"attributes_msg_userid_already_exists": "O ID de utilizador já existe para este ambiente e não foi atualizado.",
"contact_deleted_successfully": "Contacto eliminado com sucesso",
"contact_not_found": "Nenhum contacto encontrado",
"contacts_table_refresh": "Atualizar contactos",
"contacts_table_refresh_success": "Contactos atualizados com sucesso",
"create_attribute": "Criar atributo",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "Conectează",
"connect_formbricks": "Conectează Formbricks",
"connected": "Conectat",
"contact": "Contact",
"contacts": "Contacte",
"continue": "Continuă",
"copied": "Copiat",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(copie {copyNumber})",
"e_commerce": "Comerț electronic",
"edit": "Editare",
"elements": "Elemente",
"email": "Email",
"ending_card": "Cardul de finalizare",
"enter_url": "Introduceți URL-ul",
"enterprise_license": "Licență Întreprindere",
"environment": "Mediu",
"environment_not_found": "Mediul nu a fost găsit",
"environment_notice": "Te afli în prezent în mediul {environment}",
"error": "Eroare",
"error_component_description": "Această resursă nu există sau nu aveți drepturile necesare pentru a o accesa.",
@@ -261,6 +262,8 @@
"invalid_file_type": "Tip de fișier nevalid",
"invite": "Invită",
"invite_them": "Invită-i",
"javascript_required": "JavaScript necesar",
"javascript_required_description": "Formbricks necesită JavaScript pentru a funcționa corect. Te rugăm să activezi JavaScript în setările browserului tău pentru a continua.",
"key": "Cheie",
"label": "Etichetă",
"language": "Limba",
@@ -281,7 +284,9 @@
"marketing": "Marketing",
"members": "Membri",
"members_and_teams": "Membri și echipe",
"membership": "Abonament",
"membership_not_found": "Apartenența nu a fost găsită",
"meta": "Meta",
"metadata": "Metadate",
"mobile_overlay_app_works_best_on_desktop": "Formbricks funcționează cel mai bine pe un ecran mai mare. Pentru a gestiona sau crea chestionare, treceți la un alt dispozitiv.",
"mobile_overlay_surveys_look_good": "Nu vă faceți griji chestionarele dumneavoastră arată grozav pe orice dispozitiv și dimensiune a ecranului!",
@@ -321,10 +326,9 @@
"or": "sau",
"organization": "Organizație",
"organization_id": "ID Organizație",
"organization_not_found": "Organizația nu a fost găsită",
"organization_settings": "Setări Organizație",
"organization_teams_not_found": "Echipele organizației nu au fost găsite",
"other": "Altele",
"other_filters": "Alte Filtre",
"others": "Altele",
"overlay_color": "Culoare overlay",
"overview": "Prezentare generală",
@@ -416,7 +420,6 @@
"survey_id": "ID Chestionar",
"survey_languages": "Limbi chestionar",
"survey_live": "Chestionar activ",
"survey_not_found": "Sondajul nu a fost găsit",
"survey_paused": "Chestionar oprit.",
"survey_type": "Tip Chestionar",
"surveys": "Sondaje",
@@ -431,7 +434,6 @@
"team_name": "Nume echipă",
"team_role": "Rol în echipă",
"teams": "Echipe",
"teams_not_found": "Echipele nu au fost găsite",
"text": "Text",
"time": "Timp",
"time_to_finish": "Timp până la finalizare",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "Utilizator",
"user_id": "ID Utilizator",
"user_not_found": "Utilizatorul nu a fost găsit",
"variable": "Variabilă",
"variable_ids": "ID-uri variabile",
"variables": "Variante",
@@ -471,14 +472,13 @@
"weeks": "săptămâni",
"welcome_card": "Card de bun venit",
"workflows": "Workflows",
"workspace": "Spațiu de lucru",
"workspace_configuration": "Configurare workspace",
"workspace_created_successfully": "Spațiul de lucru a fost creat cu succes",
"workspace_creation_description": "Organizează sondajele în workspaces pentru un control mai bun al accesului.",
"workspace_id": "ID workspace",
"workspace_name": "Nume workspace",
"workspace_name_placeholder": "ex: Formbricks",
"workspace_not_found": "Workspace-ul nu a fost găsit",
"workspace_permission_not_found": "Permisiunea pentru workspace nu a fost găsită",
"workspaces": "Workspaces",
"years": "ani",
"you": "Tu",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "A fost creat un nou atribut „{key}” cu tipul „{dataType}”",
"attributes_msg_userid_already_exists": "ID-ul de utilizator există deja pentru acest mediu și nu a fost actualizat.",
"contact_deleted_successfully": "Contact șters cu succes",
"contact_not_found": "Nu a fost găsit niciun contact",
"contacts_table_refresh": "Reîmprospătare contacte",
"contacts_table_refresh_success": "Contactele au fost actualizate cu succes",
"create_attribute": "Creează atribut",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "Подключить",
"connect_formbricks": "Подключить Formbricks",
"connected": "Подключено",
"contact": "Контакт",
"contacts": "Контакты",
"continue": "Продолжить",
"copied": "Скопировано",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(копия {copyNumber})",
"e_commerce": "E-Commerce",
"edit": "Редактировать",
"elements": "Элементы",
"email": "Email",
"ending_card": "Завершающая карточка",
"enter_url": "Введите URL",
"enterprise_license": "Корпоративная лицензия",
"environment": "Окружение",
"environment_not_found": "Среда не найдена",
"environment_notice": "В данный момент вы находитесь в среде {environment}.",
"error": "Ошибка",
"error_component_description": "Этот ресурс не существует или у вас нет необходимых прав для доступа к нему.",
@@ -261,6 +262,8 @@
"invalid_file_type": "Недопустимый тип файла",
"invite": "Пригласить",
"invite_them": "Пригласить их",
"javascript_required": "Требуется JavaScript",
"javascript_required_description": "Для корректной работы Formbricks необходим JavaScript. Пожалуйста, включите JavaScript в настройках вашего браузера, чтобы продолжить.",
"key": "Ключ",
"label": "Метка",
"language": "Язык",
@@ -281,7 +284,9 @@
"marketing": "Маркетинг",
"members": "Участники",
"members_and_teams": "Участники и команды",
"membership": "Членство",
"membership_not_found": "Участие не найдено",
"meta": "Мета",
"metadata": "Метаданные",
"mobile_overlay_app_works_best_on_desktop": "Formbricks лучше всего работает на большом экране. Для управления или создания опросов перейдите на другое устройство.",
"mobile_overlay_surveys_look_good": "Не волнуйтесь — ваши опросы отлично выглядят на любом устройстве и экране!",
@@ -321,10 +326,9 @@
"or": "или",
"organization": "Организация",
"organization_id": "ID организации",
"organization_not_found": "Организация не найдена",
"organization_settings": "Настройки организации",
"organization_teams_not_found": "Команды организации не найдены",
"other": "Другое",
"other_filters": "Другие фильтры",
"others": "Другие",
"overlay_color": "Цвет наложения",
"overview": "Обзор",
@@ -416,7 +420,6 @@
"survey_id": "ID опроса",
"survey_languages": "Языки опроса",
"survey_live": "Опрос активен",
"survey_not_found": "Опрос не найден",
"survey_paused": "Опрос приостановлен.",
"survey_type": "Тип опроса",
"surveys": "Опросы",
@@ -431,7 +434,6 @@
"team_name": "Название команды",
"team_role": "Роль в команде",
"teams": "Команды",
"teams_not_found": "Команды не найдены",
"text": "Текст",
"time": "Время",
"time_to_finish": "Время до завершения",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "Пользователь",
"user_id": "ID пользователя",
"user_not_found": "Пользователь не найден",
"variable": "Переменная",
"variable_ids": "ID переменных",
"variables": "Переменные",
@@ -471,14 +472,13 @@
"weeks": "недели",
"welcome_card": "Приветственная карточка",
"workflows": "Воркфлоу",
"workspace": "Рабочее пространство",
"workspace_configuration": "Настройка рабочего пространства",
"workspace_created_successfully": "Рабочий проект успешно создан",
"workspace_creation_description": "Организуйте опросы в рабочих пространствах для лучшего контроля доступа.",
"workspace_id": "ID рабочего пространства",
"workspace_name": "Название рабочего пространства",
"workspace_name_placeholder": "например, Formbricks",
"workspace_not_found": "Рабочее пространство не найдено",
"workspace_permission_not_found": "Разрешение на рабочее пространство не найдено",
"workspaces": "Рабочие пространства",
"years": "годы",
"you": "Вы",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "Создан новый атрибут «{key}» с типом «{dataType}»",
"attributes_msg_userid_already_exists": "Этот user ID уже существует в данной среде и не был обновлён.",
"contact_deleted_successfully": "Контакт успешно удалён",
"contact_not_found": "Такой контакт не найден",
"contacts_table_refresh": "Обновить контакты",
"contacts_table_refresh_success": "Контакты успешно обновлены",
"create_attribute": "Создать атрибут",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "Anslut",
"connect_formbricks": "Anslut Formbricks",
"connected": "Ansluten",
"contact": "Kontakt",
"contacts": "Kontakter",
"continue": "Fortsätt",
"copied": "Kopierad",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(kopia {copyNumber})",
"e_commerce": "E-handel",
"edit": "Redigera",
"elements": "Element",
"email": "E-post",
"ending_card": "Avslutningskort",
"enter_url": "Ange URL",
"enterprise_license": "Företagslicens",
"environment": "Miljö",
"environment_not_found": "Miljö hittades inte",
"environment_notice": "Du är för närvarande i {environment}-miljön.",
"error": "Fel",
"error_component_description": "Denna resurs finns inte eller så har du inte de nödvändiga rättigheterna för att komma åt den.",
@@ -261,6 +262,8 @@
"invalid_file_type": "Ogiltig filtyp",
"invite": "Bjud in",
"invite_them": "Bjud in dem",
"javascript_required": "JavaScript krävs",
"javascript_required_description": "Formbricks kräver JavaScript för att fungera korrekt. Vänligen aktivera JavaScript i dina webbläsarinställningar för att fortsätta.",
"key": "Nyckel",
"label": "Etikett",
"language": "Språk",
@@ -281,7 +284,9 @@
"marketing": "Marknadsföring",
"members": "Medlemmar",
"members_and_teams": "Medlemmar och team",
"membership": "Medlemskap",
"membership_not_found": "Medlemskap hittades inte",
"meta": "Meta",
"metadata": "Metadata",
"mobile_overlay_app_works_best_on_desktop": "Formbricks fungerar bäst på en större skärm. Byt till en annan enhet för att hantera eller bygga enkäter.",
"mobile_overlay_surveys_look_good": "Oroa dig inte dina enkäter ser bra ut på alla enheter och skärmstorlekar!",
@@ -321,10 +326,9 @@
"or": "eller",
"organization": "Organisation",
"organization_id": "Organisations-ID",
"organization_not_found": "Organisation hittades inte",
"organization_settings": "Organisationsinställningar",
"organization_teams_not_found": "Organisationsteam hittades inte",
"other": "Annat",
"other_filters": "Andra filter",
"others": "Andra",
"overlay_color": "Overlay-färg",
"overview": "Översikt",
@@ -416,7 +420,6 @@
"survey_id": "Enkät-ID",
"survey_languages": "Enkätspråk",
"survey_live": "Enkät live",
"survey_not_found": "Enkät hittades inte",
"survey_paused": "Enkät pausad.",
"survey_type": "Enkättyp",
"surveys": "Enkäter",
@@ -431,7 +434,6 @@
"team_name": "Teamnamn",
"team_role": "Teamroll",
"teams": "Åtkomstkontroll",
"teams_not_found": "Team hittades inte",
"text": "Text",
"time": "Tid",
"time_to_finish": "Tid att slutföra",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "Användare",
"user_id": "Användar-ID",
"user_not_found": "Användare hittades inte",
"variable": "Variabel",
"variable_ids": "Variabel-ID:n",
"variables": "Variabler",
@@ -471,14 +472,13 @@
"weeks": "veckor",
"welcome_card": "Välkomstkort",
"workflows": "Arbetsflöden",
"workspace": "Arbetsyta",
"workspace_configuration": "Arbetsytans konfiguration",
"workspace_created_successfully": "Arbetsytan har skapats",
"workspace_creation_description": "Organisera enkäter i arbetsytor för bättre åtkomstkontroll.",
"workspace_id": "Arbetsyte-ID",
"workspace_name": "Arbetsytans namn",
"workspace_name_placeholder": "t.ex. Formbricks",
"workspace_not_found": "Arbetsyta hittades inte",
"workspace_permission_not_found": "Arbetsytebehörighet hittades inte",
"workspaces": "Arbetsytor",
"years": "år",
"you": "Du",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "Nytt attribut ”{key}” med typen ”{dataType}” har skapats",
"attributes_msg_userid_already_exists": "Användar-ID finns redan för denna miljö och uppdaterades inte.",
"contact_deleted_successfully": "Kontakt borttagen",
"contact_not_found": "Ingen sådan kontakt hittades",
"contacts_table_refresh": "Uppdatera kontakter",
"contacts_table_refresh_success": "Kontakter uppdaterade",
"create_attribute": "Skapa attribut",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "连接",
"connect_formbricks": "连接 Formbricks",
"connected": "已连接",
"contact": "联系人",
"contacts": "联系人",
"continue": "继续",
"copied": "已复制",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(副本 {copyNumber}",
"e_commerce": "电子商务",
"edit": "编辑",
"elements": "元素",
"email": "邮箱",
"ending_card": "结尾卡片",
"enter_url": "输入 URL",
"enterprise_license": "企业 许可证",
"environment": "环境",
"environment_not_found": "环境 未找到",
"environment_notice": "你 目前 位于 {environment} 环境。",
"error": "错误",
"error_component_description": "这个资源不存在或您没有权限访问它。",
@@ -261,6 +262,8 @@
"invalid_file_type": "无效 的 文件 类型",
"invite": "邀请",
"invite_them": "邀请 他们",
"javascript_required": "需要启用 JavaScript",
"javascript_required_description": "Formbricks 需要 JavaScript 才能正常运行。请在浏览器设置中启用 JavaScript 以继续。",
"key": "键",
"label": "标签",
"language": "语言",
@@ -281,7 +284,9 @@
"marketing": "市场营销",
"members": "成员",
"members_and_teams": "成员和团队",
"membership": "会员资格",
"membership_not_found": "未找到会员资格",
"meta": "元数据",
"metadata": "元数据",
"mobile_overlay_app_works_best_on_desktop": "Formbricks 在 更大 的 屏幕 上 效果 最佳。 若 需要 管理 或 构建 调查, 请 切换 到 其他 设备。",
"mobile_overlay_surveys_look_good": "别 担心 – 您 的 调查 在 每 一 种 设备 和 屏幕 尺寸 上 看起来 都 很 棒!",
@@ -321,10 +326,9 @@
"or": "或",
"organization": "组织",
"organization_id": "组织 ID",
"organization_not_found": "组织 未找到",
"organization_settings": "组织 设置",
"organization_teams_not_found": "未找到 组织 团队",
"other": "其他",
"other_filters": "其他筛选条件",
"others": "其他",
"overlay_color": "覆盖层颜色",
"overview": "概览",
@@ -416,7 +420,6 @@
"survey_id": "调查 ID",
"survey_languages": "调查 语言",
"survey_live": "调查 运行中",
"survey_not_found": "调查 未找到",
"survey_paused": "调查 暂停。",
"survey_type": "调查 类型",
"surveys": "调查",
@@ -431,7 +434,6 @@
"team_name": "团队 名称",
"team_role": "团队角色",
"teams": "团队",
"teams_not_found": "未找到 团队",
"text": "文本",
"time": "时间",
"time_to_finish": "完成 时间",
@@ -455,7 +457,6 @@
"url": "URL",
"user": "用户",
"user_id": "用户 ID",
"user_not_found": "用户 不存在",
"variable": "变量",
"variable_ids": "变量 ID",
"variables": "变量",
@@ -471,14 +472,13 @@
"weeks": "周",
"welcome_card": "欢迎 卡片",
"workflows": "工作流",
"workspace": "工作区",
"workspace_configuration": "工作区配置",
"workspace_created_successfully": "工作区创建成功",
"workspace_creation_description": "在工作区中组织调查,以便更好地进行访问控制。",
"workspace_id": "工作区 ID",
"workspace_name": "工作区名称",
"workspace_name_placeholder": "例如:Formbricks",
"workspace_not_found": "未找到工作区",
"workspace_permission_not_found": "未找到工作区权限",
"workspaces": "工作区",
"years": "年",
"you": "你 ",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "已创建新属性“{key}”,类型为“{dataType}”",
"attributes_msg_userid_already_exists": "该环境下的用户ID已存在,未进行更新。",
"contact_deleted_successfully": "联系人 删除 成功",
"contact_not_found": "未找到此 联系人",
"contacts_table_refresh": "刷新 联系人",
"contacts_table_refresh_success": "联系人 已成功刷新",
"create_attribute": "创建属性",
+8 -9
View File
@@ -167,6 +167,7 @@
"connect": "連線",
"connect_formbricks": "連線 Formbricks",
"connected": "已連線",
"contact": "聯絡人",
"contacts": "聯絡人",
"continue": "繼續",
"copied": "已 複製",
@@ -214,12 +215,12 @@
"duplicate_copy_number": "(複製 {copyNumber}",
"e_commerce": "電子商務",
"edit": "編輯",
"elements": "元素",
"email": "電子郵件",
"ending_card": "結尾卡片",
"enter_url": "輸入 URL",
"enterprise_license": "企業授權",
"environment": "環境",
"environment_not_found": "找不到環境",
"environment_notice": "您目前在 '{'environment'}' 環境中。",
"error": "錯誤",
"error_component_description": "此資源不存在或您沒有存取權限。",
@@ -261,6 +262,8 @@
"invalid_file_type": "無效的檔案類型",
"invite": "邀請",
"invite_them": "邀請他們",
"javascript_required": "需要 JavaScript",
"javascript_required_description": "Formbricks 需要 JavaScript 才能正常運作。請在瀏覽器設定中啟用 JavaScript 以繼續使用。",
"key": "金鑰",
"label": "標籤",
"language": "語言",
@@ -281,7 +284,9 @@
"marketing": "行銷",
"members": "成員",
"members_and_teams": "成員與團隊",
"membership": "會員資格",
"membership_not_found": "找不到成員資格",
"meta": "Meta",
"metadata": "元數據",
"mobile_overlay_app_works_best_on_desktop": "Formbricks 適合在大螢幕上使用。若要管理或建立問卷,請切換到其他裝置。",
"mobile_overlay_surveys_look_good": "別擔心 -你的 問卷 在每個 裝置 和 螢幕尺寸 上 都 很出色!",
@@ -321,10 +326,9 @@
"or": "或",
"organization": "組織",
"organization_id": "組織 ID",
"organization_not_found": "找不到組織",
"organization_settings": "組織設定",
"organization_teams_not_found": "找不到組織團隊",
"other": "其他",
"other_filters": "其他篩選條件",
"others": "其他",
"overlay_color": "覆蓋層顏色",
"overview": "概覽",
@@ -416,7 +420,6 @@
"survey_id": "問卷 ID",
"survey_languages": "問卷語言",
"survey_live": "問卷已上線",
"survey_not_found": "找不到問卷",
"survey_paused": "問卷已暫停。",
"survey_type": "問卷類型",
"surveys": "問卷",
@@ -431,7 +434,6 @@
"team_name": "團隊名稱",
"team_role": "團隊角色",
"teams": "團隊",
"teams_not_found": "找不到團隊",
"text": "文字",
"time": "時間",
"time_to_finish": "完成時間",
@@ -455,7 +457,6 @@
"url": "網址",
"user": "使用者",
"user_id": "使用者 ID",
"user_not_found": "找不到使用者",
"variable": "變數",
"variable_ids": "變數 ID",
"variables": "變數",
@@ -471,14 +472,13 @@
"weeks": "週",
"welcome_card": "歡迎卡片",
"workflows": "工作流程",
"workspace": "工作區",
"workspace_configuration": "工作區設定",
"workspace_created_successfully": "工作區已成功建立",
"workspace_creation_description": "將問卷組織在工作區中,以便更好地控管存取權限。",
"workspace_id": "工作區 ID",
"workspace_name": "工作區名稱",
"workspace_name_placeholder": "例如:Formbricks",
"workspace_not_found": "找不到工作區",
"workspace_permission_not_found": "找不到工作區權限",
"workspaces": "工作區",
"years": "年",
"you": "您",
@@ -663,7 +663,6 @@
"attributes_msg_new_attribute_created": "已建立新屬性「{key}」,型別為「{dataType}」",
"attributes_msg_userid_already_exists": "此環境已存在該使用者 ID,未進行更新。",
"contact_deleted_successfully": "聯絡人已成功刪除",
"contact_not_found": "找不到此聯絡人",
"contacts_table_refresh": "重新整理聯絡人",
"contacts_table_refresh_success": "聯絡人已成功重新整理",
"create_attribute": "建立屬性",
@@ -1,6 +1,6 @@
import { Languages } from "lucide-react";
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
import { useTranslation } from "react-i18next";
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { getEnabledLanguages } from "@/lib/i18n/utils";
@@ -18,11 +18,7 @@ interface LanguageDropdownProps {
locale: TUserLocale;
}
export const LanguageDropdown = ({
survey,
setLanguage,
locale,
}: LanguageDropdownProps) => {
export const LanguageDropdown = ({ survey, setLanguage, locale }: LanguageDropdownProps) => {
const { t } = useTranslation();
const enabledLanguages = getEnabledLanguages(survey.languages ?? []);
@@ -33,7 +29,10 @@ export const LanguageDropdown = ({
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" title={t("common.select_language")} aria-label={t("common.select_language")}>
<Button
variant="secondary"
title={t("common.select_language")}
aria-label={t("common.select_language")}>
<Languages className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
@@ -2,6 +2,7 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { deleteResponse, getResponse } from "@/lib/response/service";
import { createTag } from "@/lib/tag/service";
import { addTagToRespone, deleteTagOnResponse } from "@/lib/tagOnResponse/service";
@@ -68,7 +69,7 @@ export const createTagToResponseAction = authenticatedActionClient
const tagEnvironment = await getTag(parsedInput.tagId);
if (!responseEnvironmentId || !tagEnvironment) {
throw new Error("Environment not found");
throw new ResourceNotFoundError("Environment", null);
}
if (responseEnvironmentId !== tagEnvironment.environmentId) {
@@ -113,7 +114,7 @@ export const deleteTagOnResponseAction = authenticatedActionClient
const tagEnvironment = await getTag(parsedInput.tagId);
const organizationId = await getOrganizationIdFromResponseId(parsedInput.responseId);
if (!responseEnvironmentId || !tagEnvironment) {
throw new Error("Environment not found");
throw new ResourceNotFoundError("Environment", null);
}
if (responseEnvironmentId !== tagEnvironment.environmentId) {
@@ -0,0 +1,15 @@
"use client";
import { SessionProvider } from "next-auth/react";
interface NextAuthProviderProps {
children: React.ReactNode;
sessionMaxAge: number;
}
export const NextAuthProvider = ({ children, sessionMaxAge }: NextAuthProviderProps) => {
// Refresh at 1/3 of session max age, capped at 5 minutes
const refetchInterval = Math.min(Math.max(Math.floor(sessionMaxAge / 3), 60), 300);
return <SessionProvider refetchInterval={refetchInterval}>{children}</SessionProvider>;
};
@@ -1,5 +1,6 @@
import { getServerSession } from "next-auth";
import { TEnvironment } from "@formbricks/types/environment";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TTag } from "@formbricks/types/tags";
import { DEFAULT_LOCALE } from "@/lib/constants";
@@ -35,12 +36,12 @@ export const ActivitySection = async ({ environment, contactId, environmentTags
const t = await getTranslate();
if (!session) {
throw new Error(t("common.session_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
const user = await getUser(session.user.id);
if (!user) {
throw new Error(t("common.user_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
if (!responses) {
@@ -49,7 +50,7 @@ export const ActivitySection = async ({ environment, contactId, environmentTags
const project = await getProjectByEnvironmentId(environment.id);
if (!project) {
throw new Error(t("common.workspace_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
@@ -1,3 +1,4 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getDisplaysByContactId } from "@/lib/display/service";
import { getResponsesByContactId } from "@/lib/response/service";
import { getLocale } from "@/lingodotdev/language";
@@ -17,7 +18,7 @@ export const AttributesSection = async ({ contactId }: { contactId: string }) =>
]);
if (!contact) {
throw new Error(t("environments.contacts.contact_not_found"));
throw new ResourceNotFoundError(t("common.contact"), contactId);
}
const [responses, displays] = await Promise.all([
@@ -1,3 +1,4 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getTagsByEnvironmentId } from "@/lib/tag/service";
import { getTranslate } from "@/lingodotdev/server";
import { AttributesSection } from "@/modules/ee/contacts/[contactId]/components/attributes-section";
@@ -31,7 +32,7 @@ export const SingleContactPage = async (props: {
]);
if (!contact) {
throw new Error(t("environments.contacts.contact_not_found"));
throw new ResourceNotFoundError(t("common.contact"), params.contactId);
}
const isQuotasAllowed = await getIsQuotasEnabled(organization.id);
+2 -1
View File
@@ -3,6 +3,7 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { ZContactAttributesInput } from "@formbricks/types/contact-attribute";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import {
@@ -164,7 +165,7 @@ export const updateContactAttributesAction = authenticatedActionClient
// Get contact to access environmentId for revalidation
const contact = await getContact(parsedInput.contactId);
if (!contact) {
throw new Error("Contact not found");
throw new ResourceNotFoundError("Contact", parsedInput.contactId);
}
const result = await updateContactAttributes(parsedInput.contactId, parsedInput.attributes);
@@ -1,3 +1,4 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TProject } from "@formbricks/types/project";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { getTranslate } from "@/lingodotdev/server";
@@ -20,7 +21,7 @@ export const ContactsSecondaryNavigation = async ({
project = await getProjectByEnvironmentId(environmentId);
if (!project) {
throw new Error(t("common.workspace_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { AuthorizationError } from "@formbricks/types/errors";
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
@@ -24,11 +24,11 @@ const ConfigLayout = async (props: {
]);
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), null);
}
if (!session) {
throw new Error(t("common.session_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
@@ -45,7 +45,7 @@ const ConfigLayout = async (props: {
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.workspace_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
return children;
@@ -2,7 +2,7 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { ZSegmentCreateInput, ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
import { getOrganization } from "@/lib/organization/service";
import { loadNewSegmentInSurvey } from "@/lib/survey/service";
@@ -35,7 +35,7 @@ const checkAdvancedTargetingPermission = async (organizationId: string) => {
const organization = await getOrganization(organizationId);
if (!organization) {
throw new Error("Organization not found");
throw new ResourceNotFoundError("Organization", organizationId);
}
const isContactsEnabled = await getIsContactsEnabled(organizationId);
@@ -2,7 +2,12 @@
import { z } from "zod";
import { ZId, ZUuid } from "@formbricks/types/common";
import { AuthenticationError, OperationNotAllowedError, ValidationError } from "@formbricks/types/errors";
import {
AuthenticationError,
OperationNotAllowedError,
ResourceNotFoundError,
ValidationError,
} from "@formbricks/types/errors";
import { ZMembershipUpdateInput } from "@formbricks/types/memberships";
import { IS_FORMBRICKS_CLOUD, USER_MANAGEMENT_MINIMUM_ROLE } from "@/lib/constants";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
@@ -21,7 +26,7 @@ import { getInvite } from "@/modules/organization/settings/teams/lib/invite";
export const checkRoleManagementPermission = async (organizationId: string) => {
const organization = await getOrganization(organizationId);
if (!organization) {
throw new Error("Organization not found");
throw new ResourceNotFoundError("Organization", organizationId);
}
const isAccessControlAllowed = await getAccessControlPermission(organizationId);
+2 -1
View File
@@ -4,6 +4,7 @@ import { cache as reactCache } from "react";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { DEFAULT_TEAM_ID } from "@/lib/constants";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { validateInputs } from "@/lib/utils/validate";
@@ -41,7 +42,7 @@ const getTeam = reactCache(async (teamId: string): Promise<Team> => {
});
if (!team) {
throw new Error("Team not found");
throw new ResourceNotFoundError("Team", teamId);
}
return team;
@@ -1,3 +1,4 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getTranslate } from "@/lingodotdev/server";
import { AccessView } from "@/modules/ee/teams/project-teams/components/access-view";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
@@ -15,7 +16,7 @@ export const ProjectTeams = async (props: { params: Promise<{ environmentId: str
const teams = await getTeamsByProjectId(project.id);
if (!teams) {
throw new Error(t("common.teams_not_found"));
throw new ResourceNotFoundError(t("common.teams"), null);
}
return (
@@ -1,3 +1,4 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
@@ -32,7 +33,7 @@ export const TeamsView = async ({
]);
if (!teams) {
throw new Error(t("common.teams_not_found"));
throw new ResourceNotFoundError(t("common.teams"), null);
}
const buttons: [ModalButton, ModalButton] = [
@@ -301,7 +301,7 @@ describe("updateTeamDetails", () => {
updatedAt: new Date(),
});
vi.mocked(prisma.team.findUnique).mockResolvedValueOnce(null);
await expect(updateTeamDetails("t1", data)).rejects.toThrow("Team not found");
await expect(updateTeamDetails("t1", data)).rejects.toThrow("Team with ID t1 not found");
});
test("throws error if user not in org membership", async () => {
vi.mocked(prisma.team.findUnique).mockResolvedValueOnce({
@@ -309,7 +309,7 @@ export const updateTeamDetails = async (teamId: string, data: TTeamSettingsFormS
const currentTeamDetails = await getTeamDetails(teamId);
if (!currentTeamDetails) {
throw new Error("Team not found");
throw new ResourceNotFoundError("Team", teamId);
}
// Check that all users exist within the organization's membership.
@@ -2,7 +2,7 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getOrganization } from "@/lib/organization/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
@@ -45,7 +45,7 @@ export const updateProjectBrandingAction = authenticatedActionClient.inputSchema
const organization = await getOrganization(organizationId);
if (!organization) {
throw new Error("Organization not found");
throw new ResourceNotFoundError("Organization", organizationId);
}
const canRemoveBranding = await getRemoveBrandingPermission(organizationId);
+2 -2
View File
@@ -15,7 +15,7 @@ import {
import { TEmailTemplateLegalProps } from "@formbricks/email/src/types/email";
import { logger } from "@formbricks/logger";
import type { TLinkSurveyEmailData } from "@formbricks/types/email";
import { InvalidInputError } from "@formbricks/types/errors";
import { InvalidInputError, ResourceNotFoundError } 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";
@@ -237,7 +237,7 @@ export const sendResponseFinishedEmail = async (
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!organization) {
throw new Error("Organization not found");
throw new ResourceNotFoundError("Organization", null);
}
// Pre-process the element response mapping before passing to email
+15 -11
View File
@@ -4,7 +4,7 @@ import { getServerSession } from "next-auth";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { TEnvironment } from "@formbricks/types/environment";
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TMembership } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { TProject } from "@formbricks/types/project";
@@ -102,6 +102,12 @@ vi.mock("@/lib/constants", () => ({
}));
vi.mock("@formbricks/types/errors", () => ({
AuthenticationError: class AuthenticationError extends Error {
constructor(message: string) {
super(message);
this.name = "AuthenticationError";
}
},
AuthorizationError: class AuthorizationError extends Error {},
DatabaseError: class DatabaseError extends Error {},
ResourceNotFoundError: class ResourceNotFoundError extends Error {
@@ -162,22 +168,22 @@ describe("utils.ts", () => {
test("throws error if project not found", async () => {
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(null);
await expect(getEnvironmentAuth("env123")).rejects.toThrow("common.workspace_not_found");
await expect(getEnvironmentAuth("env123")).rejects.toThrow(ResourceNotFoundError);
});
test("throws error if environment not found", async () => {
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
await expect(getEnvironmentAuth("env123")).rejects.toThrow("common.environment_not_found");
await expect(getEnvironmentAuth("env123")).rejects.toThrow(ResourceNotFoundError);
});
test("throws error if session not found", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce(null);
await expect(getEnvironmentAuth("env123")).rejects.toThrow("common.session_not_found");
await expect(getEnvironmentAuth("env123")).rejects.toThrow(AuthenticationError);
});
test("throws error if organization not found", async () => {
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(null);
await expect(getEnvironmentAuth("env123")).rejects.toThrow("common.organization_not_found");
await expect(getEnvironmentAuth("env123")).rejects.toThrow(ResourceNotFoundError);
});
test("throws AuthorizationError if membership not found", async () => {
@@ -219,7 +225,7 @@ describe("utils.ts", () => {
test("throws error if organization not found", async () => {
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(null);
await expect(environmentIdLayoutChecks("env123")).rejects.toThrow("common.organization_not_found");
await expect(environmentIdLayoutChecks("env123")).rejects.toThrow(ResourceNotFoundError);
});
});
@@ -454,7 +460,7 @@ describe("utils.ts", () => {
test("throws error if session not found", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce(null);
await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow("common.session_not_found");
await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(AuthenticationError);
});
test("throws error if userId doesn't match session", async () => {
@@ -466,15 +472,13 @@ describe("utils.ts", () => {
test("throws error if user not found", async () => {
vi.mocked(getUser).mockResolvedValueOnce(null);
await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow("common.user_not_found");
await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(AuthenticationError);
});
test("throws error if environment data not found", async () => {
vi.mocked(prisma.environment.findUnique).mockResolvedValueOnce(null);
await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(
"common.environment_not_found"
);
await expect(getEnvironmentLayoutData("env123", "user123")).rejects.toThrow(ResourceNotFoundError);
});
test("throws AuthorizationError if user has no environment access", async () => {
+15 -10
View File
@@ -4,7 +4,12 @@ import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { AuthorizationError, DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import {
AuthenticationError,
AuthorizationError,
DatabaseError,
ResourceNotFoundError,
} from "@formbricks/types/errors";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
import { getEnvironment } from "@/lib/environment/service";
@@ -43,19 +48,19 @@ export const getEnvironmentAuth = reactCache(async (environmentId: string): Prom
]);
if (!project) {
throw new Error(t("common.workspace_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
if (!environment) {
throw new Error(t("common.environment_not_found"));
throw new ResourceNotFoundError(t("common.environment"), environmentId);
}
if (!session) {
throw new Error(t("common.session_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), null);
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
@@ -109,7 +114,7 @@ export const environmentIdLayoutChecks = async (environmentId: string) => {
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), null);
}
return { t, session, user, organization };
@@ -274,18 +279,18 @@ export const getEnvironmentLayoutData = reactCache(
const session = await getServerSession(authOptions);
if (!session?.user) {
throw new Error(t("common.session_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
// Verify userId matches session (safety check)
if (session.user.id !== userId) {
throw new Error("User ID mismatch with session");
throw new AuthenticationError("User ID mismatch with session");
}
// Get user first (lightweight query needed for subsequent checks)
const user = await getUser(userId); // 1 DB query
if (!user) {
throw new Error(t("common.user_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
// Authorization check before expensive data fetching
@@ -296,7 +301,7 @@ export const getEnvironmentLayoutData = reactCache(
const relationData = await getEnvironmentWithRelations(environmentId, userId);
if (!relationData) {
throw new Error(t("common.environment_not_found"));
throw new ResourceNotFoundError(t("common.environment"), environmentId);
}
const { environment, project, organization, environments, membership } = relationData;
@@ -1,5 +1,6 @@
import { getServerSession } from "next-auth";
import { describe, expect, test, vi } from "vitest";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TMembership } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
@@ -59,19 +60,19 @@ describe("getOrganizationAuth", () => {
test("throws if session is missing", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce(null);
vi.mocked(getOrganization).mockResolvedValue(mockOrg);
await expect(getOrganizationAuth("org-1")).rejects.toThrow("common.session_not_found");
await expect(getOrganizationAuth("org-1")).rejects.toThrow(AuthenticationError);
});
test("throws if organization is missing", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
vi.mocked(getOrganization).mockResolvedValue(null);
await expect(getOrganizationAuth("org-1")).rejects.toThrow("common.organization_not_found");
await expect(getOrganizationAuth("org-1")).rejects.toThrow(ResourceNotFoundError);
});
test("throws if membership is missing", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
vi.mocked(getOrganization).mockResolvedValue(mockOrg);
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(null);
await expect(getOrganizationAuth("org-1")).rejects.toThrow("common.membership_not_found");
await expect(getOrganizationAuth("org-1")).rejects.toThrow(ResourceNotFoundError);
});
});
+4 -3
View File
@@ -1,5 +1,6 @@
import { getServerSession } from "next-auth";
import { cache as reactCache } from "react";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import { getOrganization } from "@/lib/organization/service";
@@ -23,16 +24,16 @@ export const getOrganizationAuth = reactCache(async (organizationId: string): Pr
]);
if (!session) {
throw new Error(t("common.session_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), organizationId);
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
if (!currentUserMembership) {
throw new Error(t("common.membership_not_found"));
throw new ResourceNotFoundError(t("common.membership"), null);
}
const { isMember, isOwner, isManager, isBilling } = getAccessFlags(currentUserMembership?.role);
@@ -1,7 +1,7 @@
"use client";
import { useTranslation } from "react-i18next";
import { type JSX, useState } from "react";
import { useTranslation } from "react-i18next";
import { TActionClass } from "@formbricks/types/action-classes";
import { TEnvironment } from "@formbricks/types/environment";
import { ActionDetailModal } from "./ActionDetailModal";
@@ -2,7 +2,7 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { ZProjectUpdateInput } from "@formbricks/types/project";
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
import { getOrganization } from "@/lib/organization/service";
@@ -48,7 +48,7 @@ export const updateProjectAction = authenticatedActionClient.inputSchema(ZUpdate
const organization = await getOrganization(organizationId);
if (!organization) {
throw new Error("Organization not found");
throw new ResourceNotFoundError("Organization", organizationId);
}
const canRemoveBranding = await getRemoveBrandingPermission(organizationId);
@@ -1,4 +1,5 @@
import { getServerSession } from "next-auth";
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TProject } from "@formbricks/types/project";
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getUserProjects } from "@/lib/project/service";
@@ -22,11 +23,11 @@ export const DeleteProject = async ({
const t = await getTranslate();
const session = await getServerSession(authOptions);
if (!session) {
throw new Error(t("common.session_not_found"));
throw new AuthenticationError(t("common.not_authenticated"));
}
const organization = await getOrganizationByEnvironmentId(environmentId);
if (!organization) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), null);
}
const availableProjects = organization ? await getUserProjects(session.user.id, organization.id) : null;
@@ -1,3 +1,4 @@
import { AuthenticationError } from "@formbricks/types/errors";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { getUser } from "@/lib/user/service";
import { getTranslate } from "@/lingodotdev/server";
@@ -16,7 +17,7 @@ export const LanguagesPage = async (props: { params: Promise<{ environmentId: st
const user = await getUser(session.user.id);
if (!user) {
throw new Error("User not found");
throw new AuthenticationError(t("common.not_authenticated"));
}
return (
@@ -1,3 +1,4 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { cn } from "@/lib/cn";
import { IS_STORAGE_CONFIGURED, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@/lib/constants";
@@ -24,7 +25,7 @@ export const ProjectLookSettingsPage = async (props: { params: Promise<{ environ
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error("Workspace not found");
throw new ResourceNotFoundError(t("common.workspace"), null);
}
const canRemoveBranding = await getRemoveBrandingPermission(organization.id);
@@ -128,14 +128,21 @@ export const EditWelcomeCard = ({
id="welcome-card-image"
allowedFileExtensions={["png", "jpeg", "jpg", "webp", "heic"]}
environmentId={environmentId}
onFileUpload={(url: string[] | undefined, _fileType: "image" | "video") => {
if (url?.length) {
updateSurvey({ fileUrl: url[0] });
onFileUpload={(url: string[] | undefined, fileType: "image" | "video") => {
if (url?.length && url[0]) {
const update =
fileType === "video"
? { videoUrl: url[0], fileUrl: undefined }
: { fileUrl: url[0], videoUrl: undefined };
updateSurvey(update);
} else {
updateSurvey({ fileUrl: undefined });
updateSurvey({ fileUrl: undefined, videoUrl: undefined });
}
}}
fileUrl={localSurvey?.welcomeCard?.fileUrl}
videoUrl={localSurvey?.welcomeCard?.videoUrl}
isVideoAllowed={true}
maxSizeInMB={5}
isStorageConfigured={isStorageConfigured}
/>
</div>
+3 -2
View File
@@ -1,3 +1,4 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import {
DEFAULT_LOCALE,
IS_FORMBRICKS_CLOUD,
@@ -60,12 +61,12 @@ export const SurveyEditorPage = async (props: {
]);
if (!projectWithTeamIds) {
throw new Error(t("common.workspace_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
const organizationBilling = await getOrganizationBilling(projectWithTeamIds.organizationId);
if (!organizationBilling) {
throw new Error(t("common.organization_not_found"));
throw new ResourceNotFoundError(t("common.organization"), projectWithTeamIds.organizationId);
}
const isSurveyCreationDeletionDisabled = isMember && hasReadAccess;
+2 -1
View File
@@ -2,6 +2,7 @@ import { PlusIcon } from "lucide-react";
import { Metadata } from "next";
import Link from "next/link";
import { redirect } from "next/navigation";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { DEFAULT_LOCALE, SURVEYS_PER_PAGE } from "@/lib/constants";
import { getPublicDomain } from "@/lib/getPublicUrl";
import { getUserLocale } from "@/lib/user/service";
@@ -33,7 +34,7 @@ export const SurveysPage = async ({ params: paramsProps }: SurveyTemplateProps)
const project = await getProjectWithTeamIdsByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.workspace_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
const { session, isBilling, environment, isReadOnly } = await getEnvironmentAuth(params.environmentId);
+2 -1
View File
@@ -1,4 +1,5 @@
import { redirect } from "next/navigation";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getPublicDomain } from "@/lib/getPublicUrl";
import { getTranslate } from "@/lingodotdev/server";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
@@ -21,7 +22,7 @@ export const SurveyTemplatesPage = async (props: SurveyTemplateProps) => {
const project = await getProjectWithTeamIdsByEnvironmentId(environmentId);
if (!project) {
throw new Error(t("common.workspace_not_found"));
throw new ResourceNotFoundError(t("common.workspace"), null);
}
if (isReadOnly) {
@@ -2,7 +2,7 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Column, Table, flexRender } from "@tanstack/react-table";
import { Column, HeaderContext, Table, flexRender } from "@tanstack/react-table";
import { GripVertical } from "lucide-react";
import { TSurvey } from "@formbricks/types/surveys/types";
import { Switch } from "@/modules/ui/components/switch";
@@ -24,11 +24,10 @@ export const DataTableSettingsModalItem = <T,>({ column, table }: DataTableSetti
zIndex: isDragging ? 10 : 1,
};
// Find the header for this column from the table's header groups
const header = table
.getHeaderGroups()
.flatMap((headerGroup) => headerGroup.headers)
.find((h) => h.column.id === column.id);
// Build a minimal header context so we can render the column's header definition regardless of
// whether the column is currently visible. getHeaderGroups() only includes visible columns, so
// hidden columns would fall back to rendering the raw column ID without this approach.
const headerContext = { column, header: null, table } as unknown as HeaderContext<any, unknown>;
return (
<div ref={setNodeRef} style={style} id={column.id}>
@@ -40,7 +39,7 @@ export const DataTableSettingsModalItem = <T,>({ column, table }: DataTableSetti
<button type="button" aria-label="Reorder column" onClick={(e) => e.preventDefault()}>
<GripVertical className="h-4 w-4" />
</button>
{header ? flexRender(column.columnDef.header, header.getContext()) : column.id}
{flexRender(column.columnDef.header, headerContext)}
</div>
<Switch
id={column.id}
@@ -13,7 +13,6 @@ import { Table } from "@tanstack/react-table";
import { SettingsIcon } from "lucide-react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { TSurvey } from "@formbricks/types/surveys/types";
import {
Dialog,
DialogBody,
@@ -30,7 +29,6 @@ interface DataTableSettingsModalProps<T> {
table: Table<T>;
columnOrder: string[];
handleDragEnd: (event: DragEndEvent) => void;
survey?: TSurvey;
}
export const DataTableSettingsModal = <T,>({
@@ -39,7 +37,6 @@ export const DataTableSettingsModal = <T,>({
table,
columnOrder,
handleDragEnd,
survey,
}: DataTableSettingsModalProps<T>) => {
const { t } = useTranslation();
const sensors = useSensors(
@@ -72,9 +69,7 @@ export const DataTableSettingsModal = <T,>({
if (columnId === "select" || columnId === "createdAt") return;
const column = tableColumns.find((column) => column.id === columnId);
if (!column) return null;
return (
<DataTableSettingsModalItem column={column} table={table} key={column.id} survey={survey} />
);
return <DataTableSettingsModalItem column={column} table={table} key={column.id} />;
})}
</SortableContext>
</DndContext>
@@ -1,4 +1,5 @@
import Link from "next/link";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { WEBAPP_URL } from "@/lib/constants";
import { getEnvironment, getEnvironments } from "@/lib/environment/service";
import { getTranslate } from "@/lingodotdev/server";
@@ -13,7 +14,7 @@ export const EnvironmentNotice = async ({ environmentId, subPageUrl }: Environme
const [t, environment] = await Promise.all([getTranslate(), getEnvironment(environmentId)]);
if (!environment) {
throw new Error("Environment not found");
throw new ResourceNotFoundError(t("common.environment"), environmentId);
}
const environments = await getEnvironments(environment.projectId);
@@ -22,7 +23,7 @@ export const EnvironmentNotice = async ({ environmentId, subPageUrl }: Environme
);
if (!otherEnvironment) {
throw new Error("Other environment not found");
throw new ResourceNotFoundError(t("common.environment"), null);
}
const currentEnvironmentLabel = t(
@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import { ArrowUpFromLineIcon } from "lucide-react";
import React from "react";
import { useTranslation } from "react-i18next";
import { TAllowedFileExtension } from "@formbricks/types/storage";
import { cn } from "@/lib/cn";
import { showStorageNotConfiguredToast } from "@/modules/ui/components/storage-not-configured-toast/lib/utils";
@@ -310,7 +310,11 @@ export const PreviewSurvey = ({
setIsFullScreenPreview(true);
}
}}
aria-label={isFullScreenPreview ? t("environments.surveys.edit.shrink_preview") : t("environments.surveys.edit.expand_preview")}></button>
aria-label={
isFullScreenPreview
? t("environments.surveys.edit.shrink_preview")
: t("environments.surveys.edit.expand_preview")
}></button>
</div>
<div className="ml-4 flex w-full justify-between font-mono text-sm text-slate-400">
<p>
@@ -32,21 +32,13 @@ icon: "github"
pnpm install
```
4. **Create a `.env` file from the template:**
4. **Create a development `.env` file and generate the required secrets:**
```bash
cp .env.example .env
pnpm dev:setup
```
5. **Generate & set the required secrets:**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):**
5. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):**
This step generates an `AGENTS.md` file at the repository root that provides Next.js documentation context for AI coding assistants (e.g. Cursor, GitHub Copilot). It runs `npx @next/codemod agents-md` under the hood. Re-run it whenever you upgrade Next.js.
@@ -54,7 +46,7 @@ icon: "github"
pnpm agents:update
```
7. **Launch the development setup:**
6. **Launch the development setup:**
```bash
pnpm go
```
+4 -12
View File
@@ -32,21 +32,13 @@ icon: "code"
pnpm install
```
4. **Create a `.env` file:**
4. **Create a development `.env` file and generate the required secrets:**
```bash
cp .env.example .env
pnpm dev:setup
```
5. **Generate & set secret values:**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):**
5. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):**
This step generates an `AGENTS.md` file at the repository root that provides Next.js documentation context for AI coding assistants (e.g. Cursor, GitHub Copilot). It runs `npx @next/codemod agents-md` under the hood. Re-run it whenever you upgrade Next.js.
@@ -54,7 +46,7 @@ icon: "code"
pnpm agents:update
```
7. **Run the development setup:**
6. **Run the development setup:**
```bash
pnpm go
```
+4 -12
View File
@@ -34,21 +34,13 @@ Here are the requirements for setting up Formbricks on Linux:
pnpm install
```
4. **Create a `.env` file based on `.env.example`:**
4. **Create a development `.env` file and generate the required secrets:**
```bash
cp .env.example .env
pnpm dev:setup
```
5. **Generate & set the secret values:**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):**
5. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):**
This step generates an `AGENTS.md` file at the repository root that provides Next.js documentation context for AI coding assistants (e.g. Cursor, GitHub Copilot). It runs `npx @next/codemod agents-md` under the hood. Re-run it whenever you upgrade Next.js.
@@ -56,7 +48,7 @@ Here are the requirements for setting up Formbricks on Linux:
pnpm agents:update
```
7. **Start the development setup:**
6. **Start the development setup:**
```bash
pnpm go
```
+4 -12
View File
@@ -34,21 +34,13 @@ icon: "apple"
pnpm install
```
4. **Create a `.env` file from the example:**
4. **Create a development `.env` file and generate the required secrets:**
```bash
cp .env.example .env
pnpm dev:setup
```
5. **Generate & set secret values (using BSD sed syntax for macOS):**
```bash
sed -i '' '/^ENCRYPTION_KEY=/s|.*|ENCRYPTION_KEY='$(openssl rand -hex 32)'|' .env
sed -i '' '/^NEXTAUTH_SECRET=/s|.*|NEXTAUTH_SECRET='$(openssl rand -hex 32)'|' .env
sed -i '' '/^CRON_SECRET=/s|.*|CRON_SECRET='$(openssl rand -hex 32)'|' .env
```
6. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):**
5. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):**
This step generates an `AGENTS.md` file at the repository root that provides Next.js documentation context for AI coding assistants (e.g. Cursor, GitHub Copilot). It runs `npx @next/codemod agents-md` under the hood. Re-run it whenever you upgrade Next.js.
@@ -56,7 +48,7 @@ icon: "apple"
pnpm agents:update
```
7. **Start the development setup:**
6. **Start the development setup:**
```bash
pnpm go
```
+4 -13
View File
@@ -37,22 +37,13 @@ icon: "windows"
pnpm install
```
4. **Create a `.env` file:**
4. **Create a development `.env` file and generate the required secrets:**
```bash
cp .env.example .env
pnpm dev:setup
```
5. **Generate & set secret values (Linux commands work in WSL2):**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):**
5. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):**
This step generates an `AGENTS.md` file at the repository root that provides Next.js documentation context for AI coding assistants (e.g. Cursor, GitHub Copilot). It runs `npx @next/codemod agents-md` under the hood. Re-run it whenever you upgrade Next.js.
@@ -60,7 +51,7 @@ icon: "windows"
pnpm agents:update
```
7. **Start the development setup:**
6. **Start the development setup:**
```bash
pnpm go
```
+2 -1
View File
@@ -42,7 +42,8 @@
"generate-translations": "pnpm i18n:web:generate && pnpm i18n:surveys:generate",
"scan-translations": "pnpm --filter @formbricks/i18n-utils scan-translations",
"i18n": "pnpm generate-translations && pnpm scan-translations",
"i18n:validate": "pnpm scan-translations"
"i18n:validate": "pnpm scan-translations",
"dev:setup": "bash scripts/setup-dev-env.sh"
},
"dependencies": {
"react": "19.2.4",
+2
View File
@@ -10,6 +10,7 @@ checksums:
common/finish: ffa7a10f71182b48fefed7135bee24fa
common/language_switch: fd72a9ada13f672f4fd5da863b22cc46
common/next: 89ddbcf710eba274963494f312bdc8a9
common/no_results_found: 5518f2865757dc73900aa03ef8be6934
common/open_in_new_tab: 6844e4922a7a40a7ee25c10ea109cdeb
common/people_responded: b685fb877090d8658db724ad07a0dbd8
common/please_retry_now_or_try_again_later: 949a3841e2eb01fa249790a42bf23aa5
@@ -22,6 +23,7 @@ checksums:
common/respondents_will_not_see_this_card: 18c3dd44d6ff6ca2310ad196b84f30d3
common/retry: 6e44d18639560596569a1278f9c83676
common/retrying: 40989361ea5f6b95897b95ac928b5bd9
common/search: fe877a75eac472fc5b188c135c78a558
common/select_option: d68a0fb9afd0817dc31b3e9cb11855cb
common/select_options: d5a80087e889848e0fed3f1be359366f
common/sending_responses: 244f1aebc3f6a101ae2f8b630d7967ec
@@ -765,6 +765,7 @@ export function Survey({
headline={localSurvey.welcomeCard.headline}
subheader={localSurvey.welcomeCard.subheader}
fileUrl={localSurvey.welcomeCard.fileUrl}
videoUrl={localSurvey.welcomeCard.videoUrl}
buttonLabel={localSurvey.welcomeCard.buttonLabel}
onSubmit={onSubmit}
survey={localSurvey}
@@ -8,6 +8,7 @@ import { ScrollableContainer } from "@/components/wrappers/scrollable-container"
import { getLocalizedValue } from "@/lib/i18n";
import { replaceRecallInfo } from "@/lib/recall";
import { calculateElementIdx, getElementsFromSurveyBlocks } from "@/lib/utils";
import { ElementMedia } from "./element-media";
import { Headline } from "./headline";
import { Subheader } from "./subheader";
@@ -15,6 +16,7 @@ interface WelcomeCardProps {
headline?: TI18nString;
subheader?: TI18nString;
fileUrl?: string;
videoUrl?: string;
buttonLabel?: TI18nString;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
survey: TJsEnvironmentStateSurvey;
@@ -69,6 +71,7 @@ export function WelcomeCard({
headline,
subheader,
fileUrl,
videoUrl,
buttonLabel,
onSubmit,
languageCode,
@@ -144,8 +147,8 @@ export function WelcomeCard({
return (
<ScrollableContainer fullSizeCards={fullSizeCards}>
<div>
{fileUrl ? (
<img src={fileUrl} className="mb-8 max-h-96 w-1/4 object-contain" alt={t("common.company_logo")} />
{fileUrl || videoUrl ? (
<ElementMedia imgUrl={fileUrl} videoUrl={videoUrl} altText={t("common.company_logo")} />
) : null}
<Headline
+10 -3
View File
@@ -41,9 +41,16 @@ export const getLocalizedValue = (
* This ensures translations are always available, even when called from API routes
*/
export const getTranslations = (languageCode: string): TFunction => {
// "default" is a Formbricks-internal language identifier, not a valid i18next locale.
// When "default" is passed, use the current i18n language (which was already resolved
// to a real locale by the I18nProvider or LanguageSwitch). Calling
// i18n.changeLanguage("default") would cause i18next to fall back to "en", resetting
// the user's selected language (see issue #7515).
const resolvedCode = languageCode === "default" ? i18n.language : languageCode;
// Ensure the language is set (i18n.changeLanguage is synchronous when resources are already loaded)
if (i18n.language !== languageCode) {
i18n.changeLanguage(languageCode);
if (i18n.language !== resolvedCode) {
i18n.changeLanguage(resolvedCode);
}
return i18n.getFixedT(languageCode);
return i18n.getFixedT(resolvedCode);
};
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
readonly ENV_TEMPLATE_PATH="${REPO_ROOT}/.env.example"
readonly ENV_PATH="${REPO_ROOT}/.env"
readonly REQUIRED_GENERATED_KEYS=("ENCRYPTION_KEY" "NEXTAUTH_SECRET" "CRON_SECRET")
TEMP_FILE=""
cleanup() {
if [[ -n "${TEMP_FILE}" && -f "${TEMP_FILE}" ]]; then
rm -f "${TEMP_FILE}"
fi
}
trap cleanup EXIT
log() {
printf '%s\n' "$1"
}
fail() {
printf 'Error: %s\n' "$1" >&2
exit 1
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
fail "Required command not found: $1"
fi
}
ensure_prerequisites() {
require_command "awk"
require_command "mktemp"
require_command "openssl"
}
ensure_env_template_exists() {
if [[ ! -f "${ENV_TEMPLATE_PATH}" ]]; then
fail "Could not find template file at ${ENV_TEMPLATE_PATH}"
fi
}
copy_env_template_if_missing() {
if [[ -f "${ENV_PATH}" ]]; then
return 1
fi
cp "${ENV_TEMPLATE_PATH}" "${ENV_PATH}"
return 0
}
read_env_value() {
local key="$1"
awk -F= -v key="${key}" '
$0 ~ "^[[:space:]]*" key "[[:space:]]*=" {
value = substr($0, index($0, "=") + 1)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
if ((value ~ /^".*"$/) || (value ~ /^'\''.*'\''$/)) {
value = substr(value, 2, length(value) - 2)
}
print value
exit
}
' "${ENV_PATH}"
}
is_valid_encryption_key() {
local value="${1-}"
[[ ${#value} -eq 32 || "${value}" =~ ^[[:xdigit:]]{64}$ ]]
}
should_generate_secret() {
local key="$1"
local value="${2-}"
if [[ -z "${value}" ]]; then
return 0
fi
if [[ "${key}" == "ENCRYPTION_KEY" ]] && ! is_valid_encryption_key "${value}"; then
return 0
fi
return 1
}
upsert_env_value() {
local key="$1"
local value="$2"
TEMP_FILE="$(mktemp "${ENV_PATH}.tmp.XXXXXX")"
awk -v key="${key}" -v value="${value}" '
BEGIN {
replaced = 0
}
$0 ~ "^[[:space:]]*" key "[[:space:]]*=" {
print key "=" value
replaced = 1
next
}
{
print
}
END {
if (!replaced) {
print key "=" value
}
}
' "${ENV_PATH}" > "${TEMP_FILE}"
mv "${TEMP_FILE}" "${ENV_PATH}"
TEMP_FILE=""
}
main() {
local env_created="false"
local updated_keys=()
local key=""
local current_value=""
ensure_prerequisites
ensure_env_template_exists
if copy_env_template_if_missing; then
env_created="true"
fi
for key in "${REQUIRED_GENERATED_KEYS[@]}"; do
current_value="$(read_env_value "${key}")"
if should_generate_secret "${key}" "${current_value}"; then
upsert_env_value "${key}" "$(openssl rand -hex 32)"
updated_keys+=("${key}")
fi
done
if [[ "${env_created}" == "true" ]]; then
log "Created .env from .env.example."
else
log "Using existing .env."
fi
if [[ ${#updated_keys[@]} -gt 0 ]]; then
log "Updated generated secrets: ${updated_keys[*]}."
else
log ".env already contains all required generated secrets."
fi
log "Development environment file is ready."
}
main "$@"