mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-14 19:38:53 -05:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 569285bdd2 | |||
| 474be86d33 | |||
| e7ca66ed77 | |||
| 2b49dbecd3 | |||
| 6da4c6f352 | |||
| 659b240fca | |||
| 19c0b1d14d | |||
| b4472f48e9 | |||
| d197271771 | |||
| 37f652c70e |
@@ -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 (
|
||||
|
||||
+2
-1
@@ -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([
|
||||
|
||||
+2
-1
@@ -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}</>;
|
||||
|
||||
+4
-3
@@ -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";
|
||||
|
||||
+2
-1
@@ -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}</>;
|
||||
|
||||
-1
@@ -300,7 +300,6 @@ export const ResponseTable = ({
|
||||
<DataTableSettingsModal
|
||||
open={isTableSettingsModalOpen}
|
||||
setOpen={setIsTableSettingsModalOpen}
|
||||
survey={survey}
|
||||
table={table}
|
||||
columnOrder={columnOrder}
|
||||
handleDragEnd={handleDragEnd}
|
||||
|
||||
+5
-4
@@ -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);
|
||||
|
||||
+3
-2
@@ -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);
|
||||
|
||||
+54
-65
@@ -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)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+24
-112
@@ -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++) {
|
||||
|
||||
+5
-4
@@ -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);
|
||||
|
||||
|
||||
+26
-1
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "属性を作成",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Создать атрибут",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "创建属性",
|
||||
|
||||
@@ -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": "建立屬性",
|
||||
|
||||
+6
-7
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+6
-7
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Executable
+163
@@ -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 "$@"
|
||||
Reference in New Issue
Block a user