mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
Compare commits
11 Commits
4.1.0-rc.2
...
cursor/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0799cb81c0 | ||
|
|
600b793641 | ||
|
|
cde03b6997 | ||
|
|
00371bfb01 | ||
|
|
6be6782531 | ||
|
|
3ae4f8aa68 | ||
|
|
3d3c69a92b | ||
|
|
b1b94eaa66 | ||
|
|
67cc96449d | ||
|
|
bf41a53b86 | ||
|
|
26292ecf39 |
6
.github/workflows/formbricks-release.yml
vendored
6
.github/workflows/formbricks-release.yml
vendored
@@ -89,7 +89,7 @@ jobs:
|
||||
- check-latest-release
|
||||
with:
|
||||
IS_PRERELEASE: ${{ github.event.release.prerelease }}
|
||||
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
|
||||
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest }}
|
||||
|
||||
docker-build-cloud:
|
||||
name: Build & push Formbricks Cloud to ECR
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
with:
|
||||
image_tag: ${{ needs.docker-build-community.outputs.VERSION }}
|
||||
IS_PRERELEASE: ${{ github.event.release.prerelease }}
|
||||
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
|
||||
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest }}
|
||||
needs:
|
||||
- check-latest-release
|
||||
- docker-build-community
|
||||
@@ -154,4 +154,4 @@ jobs:
|
||||
release_tag: ${{ github.event.release.tag_name }}
|
||||
commit_sha: ${{ github.sha }}
|
||||
is_prerelease: ${{ github.event.release.prerelease }}
|
||||
make_latest: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
|
||||
make_latest: ${{ needs.check-latest-release.outputs.is_latest }}
|
||||
|
||||
@@ -96,7 +96,6 @@ export const SurveyAnalysisCTA = ({
|
||||
const duplicateSurveyAndRoute = async (surveyId: string) => {
|
||||
setLoading(true);
|
||||
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
|
||||
environmentId: environment.id,
|
||||
surveyId: surveyId,
|
||||
targetEnvironmentId: environment.id,
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"locale": {
|
||||
"source": "en-US",
|
||||
"targets": ["de-DE", "fr-FR", "ja-JP", "pt-BR", "pt-PT", "ro-RO", "zh-Hans-CN", "zh-Hant-TW"]
|
||||
"targets": ["de-DE", "fr-FR", "ja-JP", "pt-BR", "pt-PT", "ro-RO", "zh-Hans-CN", "zh-Hant-TW", "nl-NL"]
|
||||
},
|
||||
"version": 1.8
|
||||
}
|
||||
|
||||
@@ -915,15 +915,12 @@ checksums:
|
||||
environments/settings/billing/manage_subscription: 31cafd367fc70d656d8dd979d537dc96
|
||||
environments/settings/billing/monthly: 818f1192e32bb855597f930d3e78806e
|
||||
environments/settings/billing/monthly_identified_users: 0795735f6b241d31edac576a77dd7e55
|
||||
environments/settings/billing/per_month: 64e96490ee2d7811496cf04adae30aa4
|
||||
environments/settings/billing/per_year: bf02408d157486e53c15a521a5645617
|
||||
environments/settings/billing/plan_upgraded_successfully: 52e2a258cc9ca8a512c288bf6f18cf37
|
||||
environments/settings/billing/premium_support_with_slas: 2e33d4442c16bfececa6cae7b2081e5d
|
||||
environments/settings/billing/remove_branding: 88b6b818750e478bfa153b33dd658280
|
||||
environments/settings/billing/startup: 4c4ac5a0b9dc62100bca6c6465f31c4c
|
||||
environments/settings/billing/startup_description: 964fcb2c77f49b80266c94606e3f4506
|
||||
environments/settings/billing/switch_plan: fb3e1941051a4273ca29224803570f4b
|
||||
environments/settings/billing/switch_plan_confirmation_text: 910a6df56964619975c6ed5651a55db7
|
||||
environments/settings/billing/team_access_roles: 1cc4af14e589f6c09ab92a4f21958049
|
||||
environments/settings/billing/unable_to_upgrade_plan: 50fc725609411d139e534c85eeb2879e
|
||||
environments/settings/billing/unlimited_miu: 29c3f5bd01c2a09fdf1d3601665ce90f
|
||||
|
||||
@@ -170,6 +170,7 @@ export const AVAILABLE_LOCALES: TUserLocale[] = [
|
||||
"de-DE",
|
||||
"pt-BR",
|
||||
"fr-FR",
|
||||
"nl-NL",
|
||||
"zh-Hant-TW",
|
||||
"pt-PT",
|
||||
"ro-RO",
|
||||
|
||||
@@ -137,6 +137,7 @@ export const appLanguages = [
|
||||
"ro-RO": "Engleză (SUA)",
|
||||
"ja-JP": "英語(米国)",
|
||||
"zh-Hans-CN": "英语(美国)",
|
||||
"nl-NL": "Engels (VS)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -151,6 +152,7 @@ export const appLanguages = [
|
||||
"ro-RO": "Germană",
|
||||
"ja-JP": "ドイツ語",
|
||||
"zh-Hans-CN": "德语",
|
||||
"nl-NL": "Duits",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -165,6 +167,7 @@ export const appLanguages = [
|
||||
"ro-RO": "Portugheză (Brazilia)",
|
||||
"ja-JP": "ポルトガル語(ブラジル)",
|
||||
"zh-Hans-CN": "葡萄牙语(巴西)",
|
||||
"nl-NL": "Portugees (Brazilië)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -179,6 +182,7 @@ export const appLanguages = [
|
||||
"ro-RO": "Franceză",
|
||||
"ja-JP": "フランス語",
|
||||
"zh-Hans-CN": "法语",
|
||||
"nl-NL": "Frans",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -193,6 +197,7 @@ export const appLanguages = [
|
||||
"ro-RO": "Chineză (Tradicională)",
|
||||
"ja-JP": "中国語(繁体字)",
|
||||
"zh-Hans-CN": "繁体中文",
|
||||
"nl-NL": "Chinees (Traditioneel)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -207,6 +212,7 @@ export const appLanguages = [
|
||||
"ro-RO": "Portugheză (Portugalia)",
|
||||
"ja-JP": "ポルトガル語(ポルトガル)",
|
||||
"zh-Hans-CN": "葡萄牙语(葡萄牙)",
|
||||
"nl-NL": "Portugees (Portugal)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -221,6 +227,7 @@ export const appLanguages = [
|
||||
"ro-RO": "Română",
|
||||
"ja-JP": "ルーマニア語",
|
||||
"zh-Hans-CN": "罗马尼亚语",
|
||||
"nl-NL": "Roemeens",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -235,6 +242,7 @@ export const appLanguages = [
|
||||
"ro-RO": "Japoneză",
|
||||
"ja-JP": "日本語",
|
||||
"zh-Hans-CN": "日语",
|
||||
"nl-NL": "Japans",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -249,6 +257,22 @@ export const appLanguages = [
|
||||
"ro-RO": "Chineză (Simplificată)",
|
||||
"ja-JP": "中国語(簡体字)",
|
||||
"zh-Hans-CN": "简体中文",
|
||||
"nl-NL": "Chinees (Vereenvoudigd)",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "nl-NL",
|
||||
label: {
|
||||
"en-US": "Dutch",
|
||||
"de-DE": "Niederländisch",
|
||||
"pt-BR": "Holandês",
|
||||
"fr-FR": "Néerlandais",
|
||||
"zh-Hant-TW": "荷蘭語",
|
||||
"pt-PT": "Holandês",
|
||||
"ro-RO": "Olandeză",
|
||||
"ja-JP": "オランダ語",
|
||||
"zh-Hans-CN": "荷兰语",
|
||||
"nl-NL": "Nederlands",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { formatDistance, intlFormat } from "date-fns";
|
||||
import { de, enUS, fr, ja, pt, ptBR, ro, zhCN, zhTW } from "date-fns/locale";
|
||||
import { de, enUS, fr, ja, nl, pt, ptBR, ro, zhCN, zhTW } from "date-fns/locale";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
export const convertDateString = (dateString: string | null) => {
|
||||
@@ -91,6 +91,8 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
|
||||
return ptBR;
|
||||
case "fr-FR":
|
||||
return fr;
|
||||
case "nl-NL":
|
||||
return nl;
|
||||
case "zh-Hant-TW":
|
||||
return zhTW;
|
||||
case "pt-PT":
|
||||
|
||||
2911
apps/web/locales/nl-NL.json
Normal file
2911
apps/web/locales/nl-NL.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,6 @@ export const middleware = async (originalRequest: NextRequest) => {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|js|css|images|fonts|icons|public).*)",
|
||||
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|js|css|images|fonts|icons|public|animated-bgs).*)",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -62,7 +62,10 @@ const validateLanguages = (languages: Language[], t: TFunction) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the chosen alias matches an ISO identifier of a language that hasn't been added
|
||||
// Prevent choosing an alias that clashes with the ISO code of some other
|
||||
// language. Without this guard users could create ambiguous language entries
|
||||
// (e.g. alias "nl" pointing to a non-Dutch language) which later breaks the
|
||||
// dropdowns that rely on ISO identifiers.
|
||||
for (const alias of languageAliases) {
|
||||
if (iso639Languages.some((language) => language.alpha2 === alias && !languageCodes.includes(alias))) {
|
||||
toast.error(t("environments.project.languages.conflict_between_selected_alias_and_another_language"), {
|
||||
|
||||
@@ -43,8 +43,13 @@ export function LanguageSelect({ language, onLanguageChange, disabled, locale }:
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
// Most ISO entries don't ship with every locale translation, so fall back to
|
||||
// English to keep the dropdown readable for locales such as Dutch that were
|
||||
// added recently.
|
||||
const getLabelForLocale = (item: TIso639Language) => item.label[locale] ?? item.label["en-US"];
|
||||
|
||||
const filteredItems = items.filter((item) =>
|
||||
item.label[locale].toLowerCase().includes(searchTerm.toLowerCase())
|
||||
getLabelForLocale(item).toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// Focus the input when the dropdown is opened
|
||||
@@ -61,7 +66,9 @@ export function LanguageSelect({ language, onLanguageChange, disabled, locale }:
|
||||
disabled={disabled}
|
||||
onClick={toggleDropdown}
|
||||
variant="ghost">
|
||||
<span className="mr-2">{selectedOption?.label[locale] ?? t("common.select")}</span>
|
||||
<span className="mr-2">
|
||||
{selectedOption ? getLabelForLocale(selectedOption) : t("common.select")}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
<div
|
||||
@@ -84,7 +91,7 @@ export function LanguageSelect({ language, onLanguageChange, disabled, locale }:
|
||||
onClick={() => {
|
||||
handleOptionSelect(item);
|
||||
}}>
|
||||
{item.label[locale]}
|
||||
{getLabelForLocale(item)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@ export const SurveyEditorTabs = ({
|
||||
let tabsToDisplay = isCxMode ? tabsComputed.filter((tab) => tab.id !== "settings") : tabsComputed;
|
||||
|
||||
return (
|
||||
<div className="fixed z-30 flex h-12 w-full items-center justify-center border-b bg-white md:w-1/2">
|
||||
<div className="fixed z-30 flex h-12 w-full items-center justify-center border-b bg-white md:w-2/3">
|
||||
<nav className="flex h-full items-center space-x-4" aria-label="Tabs">
|
||||
{tabsToDisplay.map((tab) => (
|
||||
<button
|
||||
|
||||
@@ -176,7 +176,7 @@ export const SurveyEditor = ({
|
||||
/>
|
||||
<div className="relative z-0 flex flex-1 overflow-hidden">
|
||||
<main
|
||||
className="relative z-0 w-1/2 flex-1 overflow-y-auto bg-slate-50 focus:outline-none"
|
||||
className="relative z-0 w-full overflow-y-auto bg-slate-50 focus:outline-none md:w-2/3"
|
||||
ref={surveyEditorRef}>
|
||||
<SurveyEditorTabs
|
||||
activeId={activeView}
|
||||
@@ -260,7 +260,7 @@ export const SurveyEditor = ({
|
||||
)}
|
||||
</main>
|
||||
|
||||
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-200 bg-slate-100 shadow-inner md:flex md:flex-col">
|
||||
<aside className="group hidden w-1/3 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-200 bg-slate-100 shadow-inner md:flex md:flex-col">
|
||||
<PreviewSurvey
|
||||
survey={localSurvey}
|
||||
questionId={activeQuestionId}
|
||||
|
||||
@@ -32,6 +32,16 @@ vi.mock("@/lib/styling/constants", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock recall utility
|
||||
vi.mock("@/lib/utils/recall", () => ({
|
||||
recallToHeadline: vi.fn((headline) => headline),
|
||||
}));
|
||||
|
||||
// Mock text content extraction
|
||||
vi.mock("@formbricks/types/surveys/validation", () => ({
|
||||
getTextContent: vi.fn((text) => text),
|
||||
}));
|
||||
|
||||
describe("Metadata Utils", () => {
|
||||
// Reset all mocks before each test
|
||||
beforeEach(() => {
|
||||
@@ -173,6 +183,75 @@ describe("Metadata Utils", () => {
|
||||
WEBAPP_URL: "https://test.formbricks.com",
|
||||
}));
|
||||
});
|
||||
|
||||
test("handles welcome card headline with HTML content", async () => {
|
||||
const { getTextContent } = await import("@formbricks/types/surveys/validation");
|
||||
|
||||
const mockSurvey = {
|
||||
id: mockSurveyId,
|
||||
environmentId: mockEnvironmentId,
|
||||
name: "Test Survey",
|
||||
metadata: {},
|
||||
languages: [],
|
||||
welcomeCard: {
|
||||
enabled: true,
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
headline: {
|
||||
default: "<p>Welcome <strong>Headline</strong></p>",
|
||||
},
|
||||
html: {
|
||||
default: "Welcome Description",
|
||||
},
|
||||
} as TSurveyWelcomeCard,
|
||||
} as TSurvey;
|
||||
|
||||
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
|
||||
vi.mocked(getTextContent).mockReturnValue("Welcome Headline");
|
||||
|
||||
const result = await getBasicSurveyMetadata(mockSurveyId);
|
||||
|
||||
expect(getTextContent).toHaveBeenCalled();
|
||||
expect(result.title).toBe("Welcome Headline");
|
||||
});
|
||||
|
||||
test("handles welcome card headline with recall variables", async () => {
|
||||
const { recallToHeadline } = await import("@/lib/utils/recall");
|
||||
|
||||
const mockSurvey = {
|
||||
id: mockSurveyId,
|
||||
environmentId: mockEnvironmentId,
|
||||
name: "Test Survey",
|
||||
metadata: {},
|
||||
languages: [],
|
||||
welcomeCard: {
|
||||
enabled: true,
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
headline: {
|
||||
default: "Welcome #recall:name/fallback:User#",
|
||||
},
|
||||
html: {
|
||||
default: "Welcome Description",
|
||||
},
|
||||
} as TSurveyWelcomeCard,
|
||||
} as TSurvey;
|
||||
|
||||
vi.mocked(getSurvey).mockResolvedValue(mockSurvey);
|
||||
vi.mocked(recallToHeadline).mockReturnValue({
|
||||
default: "Welcome @User",
|
||||
});
|
||||
|
||||
const result = await getBasicSurveyMetadata(mockSurveyId);
|
||||
|
||||
expect(recallToHeadline).toHaveBeenCalledWith(
|
||||
mockSurvey.welcomeCard.headline,
|
||||
mockSurvey,
|
||||
false,
|
||||
"default"
|
||||
);
|
||||
expect(result.title).toBe("Welcome @User");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSurveyOpenGraphMetadata", () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ZSurveyFilterCriteria } from "@formbricks/types/surveys/types";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
import {
|
||||
getEnvironmentIdFromSurveyId,
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromSurveyId,
|
||||
getProjectIdFromEnvironmentId,
|
||||
@@ -50,7 +51,6 @@ export const getSurveyAction = authenticatedActionClient
|
||||
});
|
||||
|
||||
const ZCopySurveyToOtherEnvironmentAction = z.object({
|
||||
environmentId: z.string().cuid2(),
|
||||
surveyId: z.string().cuid2(),
|
||||
targetEnvironmentId: z.string().cuid2(),
|
||||
});
|
||||
@@ -66,9 +66,10 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: Record<string, any>;
|
||||
parsedInput: z.infer<typeof ZCopySurveyToOtherEnvironmentAction>;
|
||||
}) => {
|
||||
const sourceEnvironmentProjectId = await getProjectIdIfEnvironmentExists(parsedInput.environmentId);
|
||||
const sourceEnvironmentId = await getEnvironmentIdFromSurveyId(parsedInput.surveyId);
|
||||
const sourceEnvironmentProjectId = await getProjectIdIfEnvironmentExists(sourceEnvironmentId);
|
||||
const targetEnvironmentProjectId = await getProjectIdIfEnvironmentExists(
|
||||
parsedInput.targetEnvironmentId
|
||||
);
|
||||
@@ -76,13 +77,25 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
|
||||
if (!sourceEnvironmentProjectId || !targetEnvironmentProjectId) {
|
||||
throw new ResourceNotFoundError(
|
||||
"Environment",
|
||||
sourceEnvironmentProjectId ? parsedInput.targetEnvironmentId : parsedInput.environmentId
|
||||
sourceEnvironmentProjectId ? parsedInput.targetEnvironmentId : sourceEnvironmentId
|
||||
);
|
||||
}
|
||||
|
||||
const sourceEnvironmentOrganizationId = await getOrganizationIdFromEnvironmentId(sourceEnvironmentId);
|
||||
const targetEnvironmentOrganizationId = await getOrganizationIdFromEnvironmentId(
|
||||
parsedInput.targetEnvironmentId
|
||||
);
|
||||
|
||||
if (sourceEnvironmentOrganizationId !== targetEnvironmentOrganizationId) {
|
||||
throw new OperationNotAllowedError(
|
||||
"Source and target environments must be in the same organization"
|
||||
);
|
||||
}
|
||||
|
||||
// authorization check for source environment
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
organizationId: sourceEnvironmentOrganizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -96,9 +109,10 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
// authorization check for target environment
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
organizationId: targetEnvironmentOrganizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
@@ -112,12 +126,10 @@ export const copySurveyToOtherEnvironmentAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = await getOrganizationIdFromEnvironmentId(
|
||||
parsedInput.environmentId
|
||||
);
|
||||
ctx.auditLoggingCtx.organizationId = sourceEnvironmentOrganizationId;
|
||||
ctx.auditLoggingCtx.surveyId = parsedInput.surveyId;
|
||||
const result = await copySurveyToOtherEnvironment(
|
||||
parsedInput.environmentId,
|
||||
sourceEnvironmentId,
|
||||
parsedInput.surveyId,
|
||||
parsedInput.targetEnvironmentId,
|
||||
ctx.user.id
|
||||
|
||||
@@ -129,7 +129,6 @@ export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: C
|
||||
|
||||
return {
|
||||
operation: copySurveyToOtherEnvironmentAction({
|
||||
environmentId: survey.environmentId,
|
||||
surveyId: survey.id,
|
||||
targetEnvironmentId: environmentId,
|
||||
}),
|
||||
|
||||
@@ -113,7 +113,6 @@ export const SurveyDropDownMenu = ({
|
||||
setLoading(true);
|
||||
try {
|
||||
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
|
||||
environmentId,
|
||||
surveyId,
|
||||
targetEnvironmentId: environmentId,
|
||||
});
|
||||
|
||||
@@ -167,7 +167,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
|
||||
<div
|
||||
ref={ContentRef}
|
||||
data-testid="mobile-preview-container"
|
||||
className={`relative h-[90%] w-[45%] overflow-hidden rounded-[3rem] border-[6px] border-slate-400 ${getFilterStyle()}`}>
|
||||
className={`relative h-[90%] w-full overflow-hidden rounded-[3rem] border-[6px] border-slate-400 lg:w-[75%] ${getFilterStyle()}`}>
|
||||
{/* below element is use to create notch for the mobile device mockup */}
|
||||
<div className="absolute left-1/2 right-1/2 top-2 z-20 h-4 w-1/3 -translate-x-1/2 transform rounded-full bg-slate-400"></div>
|
||||
{surveyType === "link" && renderBackground()}
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
"markdown-it": "14.1.0",
|
||||
"mime-types": "3.0.1",
|
||||
"next": "15.5.6",
|
||||
"next-auth": "4.24.11",
|
||||
"next-auth": "4.24.12",
|
||||
"next-safe-action": "7.10.8",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.9",
|
||||
|
||||
@@ -211,7 +211,18 @@ vi.mock("@/lib/constants", () => ({
|
||||
SESSION_MAX_AGE: 1000,
|
||||
MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT: 100,
|
||||
MAX_OTHER_OPTION_LENGTH: 250,
|
||||
AVAILABLE_LOCALES: ["en-US", "de-DE", "pt-BR", "fr-FR", "zh-Hant-TW", "pt-PT"],
|
||||
AVAILABLE_LOCALES: [
|
||||
"en-US",
|
||||
"de-DE",
|
||||
"pt-BR",
|
||||
"fr-FR",
|
||||
"nl-NL",
|
||||
"zh-Hant-TW",
|
||||
"pt-PT",
|
||||
"ro-RO",
|
||||
"ja-JP",
|
||||
"zh-Hans-CN",
|
||||
],
|
||||
DEFAULT_LOCALE: "en-US",
|
||||
BREVO_API_KEY: "mock-brevo-api-key",
|
||||
ITEMS_PER_PAGE: 30,
|
||||
|
||||
@@ -1238,21 +1238,45 @@
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": {
|
||||
"data": {
|
||||
"hs8yd14l9h8u353tjmv6rzaw": "clicked",
|
||||
"tcgls0063n8ri7dtrbnepcmz": "Who? Who? Who?"
|
||||
},
|
||||
"finished": false,
|
||||
"meta": {
|
||||
"action": "test action",
|
||||
"source": "Postman API",
|
||||
"url": "https://postman.com"
|
||||
},
|
||||
"surveyId": "{{surveyId}}",
|
||||
"userId": "{{userId}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"example": {
|
||||
"data": {
|
||||
"hs8yd14l9h8u353tjmv6rzaw": "clicked",
|
||||
"tcgls0063n8ri7dtrbnepcmz": "Who? Who? Who?"
|
||||
"properties": {
|
||||
"data": { "additionalProperties": true, "type": "object" },
|
||||
"finished": { "type": "boolean" },
|
||||
"language": {
|
||||
"description": "Language of the response (survey should have this language enabled)",
|
||||
"enum": ["en", "de", "pt", "etc.."],
|
||||
"type": "string"
|
||||
},
|
||||
"finished": false,
|
||||
"meta": {
|
||||
"action": "test action",
|
||||
"source": "Postman API",
|
||||
"url": "https://postman.com"
|
||||
"properties": {
|
||||
"action": { "type": "string" },
|
||||
"source": { "type": "string" },
|
||||
"url": { "type": "string" }
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"surveyId": "{{surveyId}}",
|
||||
"userId": "{{userId}} (optional)"
|
||||
"surveyId": { "type": "string" },
|
||||
"userId": { "type": "string" }
|
||||
},
|
||||
"required": ["surveyId"],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
@@ -2361,20 +2385,55 @@
|
||||
},
|
||||
"noCodeConfig": {
|
||||
"description": "Configuration object required when type is 'noCode'. Defines the conditions for triggering the action. Not needed for 'code' type.",
|
||||
"example": {
|
||||
"elementSelector": {
|
||||
"cssSelector": ".button-class",
|
||||
"innerHtml": "Click me"
|
||||
},
|
||||
"type": "click",
|
||||
"urlFilters": [
|
||||
{
|
||||
"rule": "contains",
|
||||
"value": "https://www.google.com"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nullable": true,
|
||||
"properties": {
|
||||
"elementSelector": {
|
||||
"description": "Element selector (required for click type)",
|
||||
"properties": {
|
||||
"cssSelector": {
|
||||
"description": "CSS selector for the element",
|
||||
"type": "string"
|
||||
},
|
||||
"innerHtml": {
|
||||
"description": "Inner HTML text to match",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type of no-code trigger",
|
||||
"enum": ["click", "pageView", "exitIntent", "fiftyPercentScroll"],
|
||||
"type": "string"
|
||||
},
|
||||
"urlFilters": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"rule": {
|
||||
"description": "URL matching rule",
|
||||
"enum": [
|
||||
"exactMatch",
|
||||
"contains",
|
||||
"startsWith",
|
||||
"endsWith",
|
||||
"notMatch",
|
||||
"notContains",
|
||||
"matchesRegex"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"description": "URL pattern to match",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["rule", "value"],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": ["type", "urlFilters"],
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
@@ -5021,17 +5080,52 @@
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": {
|
||||
"createdAt": "2024-09-05T08:44:16.051Z",
|
||||
"data": {
|
||||
"hg508afs7lgx8nlni5dtit5u": ["Hello World"]
|
||||
},
|
||||
"finished": false,
|
||||
"language": "en",
|
||||
"surveyId": "clwj7hi7r0000vfhpfze6vjdg",
|
||||
"updatedAt": "2024-09-05T08:44:16.051Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"example": {
|
||||
"createdAt": "2024-09-05T08:44:16.051Z",
|
||||
"data": {
|
||||
"hg508afs7lgx8nlni5dtit5u": ["Hello World"]
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"description": "Creation timestamp (optional; usually set by server)",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"finished": false,
|
||||
"language": "default",
|
||||
"surveyId": "clwj7hi7r0000vfhpfze6vjdg",
|
||||
"updatedAt": "2024-09-05T08:44:16.051Z"
|
||||
"data": {
|
||||
"additionalProperties": true,
|
||||
"description": "Answers keyed by questionId; value shape depends on question type",
|
||||
"type": "object"
|
||||
},
|
||||
"finished": {
|
||||
"description": "Whether the response is marked as finished",
|
||||
"type": "boolean"
|
||||
},
|
||||
"language": {
|
||||
"description": "Language of the response (survey should have this language enabled)",
|
||||
"enum": ["en", "de", "pt", "etc.."],
|
||||
"type": "string"
|
||||
},
|
||||
"surveyId": {
|
||||
"description": "The ID of the survey this response belongs to",
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"description": "Update timestamp (optional; usually set by server)",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["surveyId"],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
@@ -5768,6 +5862,32 @@
|
||||
"allowedFileExtensions": {
|
||||
"description": "Optional. List of allowed file extensions.",
|
||||
"items": {
|
||||
"enum": [
|
||||
"heic",
|
||||
"png",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"webp",
|
||||
"pdf",
|
||||
"eml",
|
||||
"doc",
|
||||
"docx",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"ppt",
|
||||
"pptx",
|
||||
"txt",
|
||||
"csv",
|
||||
"mp4",
|
||||
"mov",
|
||||
"avi",
|
||||
"mkv",
|
||||
"webm",
|
||||
"zip",
|
||||
"rar",
|
||||
"7z",
|
||||
"tar"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
@@ -6108,113 +6228,153 @@
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"example": {
|
||||
"autoClose": null,
|
||||
"autoComplete": null,
|
||||
"createdBy": null,
|
||||
"delay": 0,
|
||||
"displayLimit": null,
|
||||
"displayOption": "displayOnce",
|
||||
"displayPercentage": null,
|
||||
"endings": [
|
||||
{
|
||||
"buttonLabel": {
|
||||
"default": "Create your own Survey"
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": {
|
||||
"autoClose": null,
|
||||
"autoComplete": null,
|
||||
"createdBy": null,
|
||||
"delay": 0,
|
||||
"displayLimit": null,
|
||||
"displayOption": "displayOnce",
|
||||
"displayPercentage": null,
|
||||
"endings": [
|
||||
{
|
||||
"buttonLabel": { "default": "Create your own Survey" },
|
||||
"buttonLink": "https://formbricks.com/signup",
|
||||
"headline": { "default": "Thank you!" },
|
||||
"id": "p73t62dgwq0cvmtt6ug0hmfc",
|
||||
"subheader": { "default": "We appreciate your feedback." },
|
||||
"type": "endScreen"
|
||||
}
|
||||
],
|
||||
"environmentId": "{{environmentId}}",
|
||||
"hiddenFields": { "enabled": false, "fieldIds": [] },
|
||||
"isVerifyEmailEnabled": false,
|
||||
"languages": [],
|
||||
"name": "Example Survey",
|
||||
"pin": null,
|
||||
"productOverwrites": null,
|
||||
"questions": [
|
||||
{
|
||||
"headline": { "default": "What would you like to know?" },
|
||||
"id": "ovpy6va1hab7fl12n913zua0",
|
||||
"inputType": "text",
|
||||
"placeholder": { "default": "Type your answer here..." },
|
||||
"required": true,
|
||||
"subheader": { "default": "This is an example survey." },
|
||||
"type": "openText"
|
||||
},
|
||||
"buttonLink": "https://formbricks.com/signup",
|
||||
"headline": {
|
||||
"default": "Thank you!"
|
||||
{
|
||||
"choices": [
|
||||
{ "id": "xpoxuu3sifk1ee8he67ctf5i", "label": { "default": "Sun \u2600\ufe0f" } },
|
||||
{ "id": "hnsovcdmxtcbly6tig1az3qc", "label": { "default": "Ocean \ud83c\udf0a" } },
|
||||
{ "id": "kcnelzdxknvwo8fq20d3nrr5", "label": { "default": "Palms \ud83c\udf34" } }
|
||||
],
|
||||
"headline": { "default": "What's important on vacay?" },
|
||||
"id": "awkn2llljy7a4oulp5t15yec",
|
||||
"required": true,
|
||||
"shuffleOption": "none",
|
||||
"type": "multipleChoiceMulti"
|
||||
}
|
||||
],
|
||||
"recontactDays": null,
|
||||
"redirectUrl": null,
|
||||
"segmentId": null,
|
||||
"showLanguageSwitch": null,
|
||||
"singleUse": { "enabled": false, "isEncrypted": true },
|
||||
"status": "inProgress",
|
||||
"styling": null,
|
||||
"surveyClosedMessage": null,
|
||||
"triggers": [],
|
||||
"type": "link",
|
||||
"welcomeCard": {
|
||||
"enabled": true,
|
||||
"fileUrl": "",
|
||||
"headline": { "default": "Welcome!" },
|
||||
"html": {
|
||||
"default": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Thanks for providing your feedback - let's go!</span></p>"
|
||||
},
|
||||
"id": "p73t62dgwq0cvmtt6ug0hmfc",
|
||||
"subheader": {
|
||||
"default": "We appreciate your feedback."
|
||||
},
|
||||
"type": "endScreen"
|
||||
"showResponseCount": false,
|
||||
"timeToFinish": false
|
||||
}
|
||||
],
|
||||
"environmentId": "{{environmentId}}",
|
||||
"hiddenFields": {
|
||||
"enabled": false,
|
||||
"fieldIds": []
|
||||
},
|
||||
"isVerifyEmailEnabled": false,
|
||||
"languages": [],
|
||||
"name": "Example Survey",
|
||||
"pin": null,
|
||||
"productOverwrites": null,
|
||||
"questions": [
|
||||
{
|
||||
"headline": {
|
||||
"default": "What would you like to know?"
|
||||
},
|
||||
"id": "ovpy6va1hab7fl12n913zua0",
|
||||
"inputType": "text",
|
||||
"placeholder": {
|
||||
"default": "Type your answer here..."
|
||||
},
|
||||
"required": true,
|
||||
"subheader": {
|
||||
"default": "This is an example survey."
|
||||
},
|
||||
"type": "openText"
|
||||
},
|
||||
{
|
||||
"choices": [
|
||||
{
|
||||
"id": "xpoxuu3sifk1ee8he67ctf5i",
|
||||
"label": {
|
||||
"default": "Sun \u2600\ufe0f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "hnsovcdmxtcbly6tig1az3qc",
|
||||
"label": {
|
||||
"default": "Ocean \ud83c\udf0a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kcnelzdxknvwo8fq20d3nrr5",
|
||||
"label": {
|
||||
"default": "Palms \ud83c\udf34"
|
||||
}
|
||||
}
|
||||
],
|
||||
"headline": {
|
||||
"default": "What's important on vacay?"
|
||||
},
|
||||
"id": "awkn2llljy7a4oulp5t15yec",
|
||||
"required": true,
|
||||
"shuffleOption": "none",
|
||||
"type": "multipleChoiceMulti"
|
||||
}
|
||||
],
|
||||
"recontactDays": null,
|
||||
"redirectUrl": null,
|
||||
"segmentId": null,
|
||||
"showLanguageSwitch": null,
|
||||
"singleUse": {
|
||||
"enabled": false,
|
||||
"isEncrypted": true
|
||||
},
|
||||
"status": "inProgress",
|
||||
"styling": null,
|
||||
"surveyClosedMessage": null,
|
||||
"triggers": [],
|
||||
"type": "link",
|
||||
"welcomeCard": {
|
||||
"enabled": true,
|
||||
"fileUrl": "",
|
||||
"headline": {
|
||||
"default": "Welcome!"
|
||||
},
|
||||
"html": {
|
||||
"default": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Thanks for providing your feedback - let's go!</span></p>"
|
||||
},
|
||||
"showResponseCount": false,
|
||||
"timeToFinish": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"properties": {
|
||||
"displayOption": {
|
||||
"enum": ["displayOnce", "displayMultiple", "respondMultiple", "displaySome"],
|
||||
"type": "string"
|
||||
},
|
||||
"environmentId": { "type": "string" },
|
||||
"languages": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"default": { "type": "boolean" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"language": {
|
||||
"description": "Language of the survey (This language should exist in your environment)",
|
||||
"enum": ["en", "de", "pt", "etc.."],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"name": { "type": "string" },
|
||||
"questions": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"date": {
|
||||
"properties": {
|
||||
"format": { "enum": ["M-d-y", "d-M-y", "y-M-d"], "type": "string" }
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"id": { "type": "string" },
|
||||
"inputType": {
|
||||
"enum": ["text", "email", "url", "number", "phone"],
|
||||
"type": "string"
|
||||
},
|
||||
"rating": {
|
||||
"properties": {
|
||||
"range": { "enum": [3, 4, 5, 6, 7, 10], "type": "integer" },
|
||||
"scale": { "enum": ["number", "smiley", "star"], "type": "string" }
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"shuffleOption": { "enum": ["none", "all", "exceptLast"], "type": "string" },
|
||||
"type": {
|
||||
"enum": [
|
||||
"address",
|
||||
"cta",
|
||||
"consent",
|
||||
"date",
|
||||
"fileUpload",
|
||||
"matrix",
|
||||
"multipleChoiceMulti",
|
||||
"multipleChoiceSingle",
|
||||
"nps",
|
||||
"openText",
|
||||
"pictureSelection",
|
||||
"rating",
|
||||
"cal",
|
||||
"ranking",
|
||||
"contactInfo"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"status": { "enum": ["draft", "inProgress", "paused", "completed"], "type": "string" },
|
||||
"type": { "enum": ["link", "app"], "type": "string" }
|
||||
},
|
||||
"required": ["environmentId", "name", "type", "status"],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
@@ -7468,11 +7628,41 @@
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"default": {
|
||||
"value": {
|
||||
"triggers": ["responseCreated", "responseUpdated", "responseFinished"],
|
||||
"url": "https://eoy8o887lmsqmhz.m.pipedream.net"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"example": {
|
||||
"triggers": ["responseCreated", "responseUpdated", "responseFinished"],
|
||||
"url": "https://eoy8o887lmsqmhz.m.pipedream.net"
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Optional name for the webhook",
|
||||
"type": "string"
|
||||
},
|
||||
"surveyIds": {
|
||||
"description": "Optional list of survey IDs to filter webhook calls",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"triggers": {
|
||||
"description": "List of events that trigger this webhook",
|
||||
"items": {
|
||||
"enum": ["responseCreated", "responseUpdated", "responseFinished"],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"url": {
|
||||
"description": "The webhook URL to call when triggers are fired",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["url", "triggers"],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
@@ -7947,7 +8137,7 @@
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://{{baseurl}}",
|
||||
"url": "https://{baseurl}",
|
||||
"variables": {
|
||||
"baseurl": {
|
||||
"default": "localhost:3000"
|
||||
|
||||
@@ -97,10 +97,44 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
surveyId:
|
||||
type: string
|
||||
description: The ID of the survey this response belongs to
|
||||
responses:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: Answers keyed by questionId; value shape depends on question type
|
||||
finished:
|
||||
type: boolean
|
||||
description: Whether the response is marked as finished
|
||||
language:
|
||||
type: string
|
||||
enum:
|
||||
[
|
||||
"en-US",
|
||||
"de-DE",
|
||||
"pt-BR",
|
||||
"fr-FR",
|
||||
"zh-Hant-TW",
|
||||
"pt-PT",
|
||||
"ro-RO",
|
||||
"ja-JP",
|
||||
"zh-Hans-CN",
|
||||
]
|
||||
description: Locale of the response
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
action: { type: string }
|
||||
source: { type: string }
|
||||
url: { type: string }
|
||||
description: Optional metadata about the response
|
||||
required: ["surveyId"]
|
||||
example:
|
||||
surveyId: survey123
|
||||
responses: {}
|
||||
type: object
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
@@ -136,9 +170,15 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
attributes:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
description: Key-value pairs of contact attributes
|
||||
required: ["attributes"]
|
||||
example:
|
||||
attributes: {}
|
||||
type: object
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
|
||||
@@ -102,6 +102,7 @@ When PUBLIC_URL is configured, the following routes are automatically served fro
|
||||
- `/fonts/*` - Font files
|
||||
- `/icons/*` - Icon assets
|
||||
- `/public/*` - Public static files
|
||||
- `/animated-bgs/*` - Animated Background assets
|
||||
|
||||
#### Storage Routes
|
||||
|
||||
|
||||
@@ -117,6 +117,191 @@ Please take a look at our [migration guide](/self-hosting/advanced/migration) fo
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Optional: Adding MinIO for File Storage
|
||||
|
||||
MinIO provides S3-compatible object storage for file uploads in Formbricks. If you want to enable features like image uploads, file uploads in surveys, or custom logos, you can add MinIO to your Docker setup.
|
||||
|
||||
<Note>
|
||||
For detailed information about file storage options and configuration, see our [File Uploads
|
||||
Configuration](/self-hosting/configuration/file-uploads) guide.
|
||||
</Note>
|
||||
|
||||
<Warning>
|
||||
**For production deployments with HTTPS**, use the [one-click setup script](/self-hosting/setup/one-click)
|
||||
which automatically configures MinIO with Traefik, SSL certificates, and a subdomain (required for MinIO in
|
||||
production). The setups below are suitable for local development or testing only.
|
||||
</Warning>
|
||||
|
||||
### Quick Start: Using docker-compose.dev.yml
|
||||
|
||||
The fastest way to test MinIO with Formbricks is to use the included `docker-compose.dev.yml` which already has MinIO pre-configured.
|
||||
|
||||
1. **Start MinIO and Services**
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
This starts PostgreSQL, Valkey (Redis), MinIO, and Mailhog.
|
||||
|
||||
2. **Access MinIO Console**
|
||||
|
||||
Open http://localhost:9001 in your browser.
|
||||
|
||||
Login credentials:
|
||||
|
||||
- Username: `devminio`
|
||||
- Password: `devminio123`
|
||||
|
||||
3. **Create Bucket**
|
||||
|
||||
- Click "Buckets" in the left sidebar
|
||||
- Click "Create Bucket"
|
||||
- Name it: `formbricks`
|
||||
|
||||
4. **Configure Formbricks**
|
||||
|
||||
Update your `.env` file or environment variables with MinIO configuration:
|
||||
|
||||
```bash
|
||||
# MinIO S3 Storage
|
||||
S3_ACCESS_KEY="devminio"
|
||||
S3_SECRET_KEY="devminio123"
|
||||
S3_REGION="us-east-1"
|
||||
S3_BUCKET_NAME="formbricks"
|
||||
S3_ENDPOINT_URL="http://localhost:9000"
|
||||
S3_FORCE_PATH_STYLE="1"
|
||||
```
|
||||
|
||||
5. **Verify in MinIO Console**
|
||||
|
||||
After uploading files in Formbricks, view them at http://localhost:9001:
|
||||
|
||||
- Navigate to Buckets → formbricks → Browse
|
||||
- Your uploaded files will appear here
|
||||
|
||||
<Note>
|
||||
The `docker-compose.dev.yml` file includes MinIO with console access on port 9001, making it easy to
|
||||
visually verify file uploads. This is the recommended approach for development and testing.
|
||||
</Note>
|
||||
|
||||
### Manual MinIO Setup (Custom Configuration)
|
||||
|
||||
<Note>
|
||||
<strong>Recommended:</strong> If you can, use <code>docker-compose.dev.yml</code> for the fastest path. Use
|
||||
this manual approach only when you need to integrate MinIO into an existing <code>docker-compose.yml</code>{" "}
|
||||
or customize settings.
|
||||
</Note>
|
||||
|
||||
If you prefer to add MinIO to your own `docker-compose.yml`, follow these steps:
|
||||
|
||||
1. **Add the MinIO service**
|
||||
|
||||
Add this service alongside your existing `formbricks` and `postgres` services:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
# ... your existing services (formbricks, postgres, redis/valkey, etc.)
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
restart: always
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: "formbricks-root"
|
||||
MINIO_ROOT_PASSWORD: "change-this-secure-password"
|
||||
ports:
|
||||
- "9000:9000" # S3 API
|
||||
- "9001:9001" # Web console
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
```
|
||||
|
||||
<Note>
|
||||
For production pinning, consider using a digest (e.g., <code>minio/minio@sha256:...</code>) and review
|
||||
periodically with <code>docker inspect minio/minio:latest</code>.
|
||||
</Note>
|
||||
|
||||
2. **Declare the MinIO volume**
|
||||
|
||||
Add (or extend) your `volumes` block:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
postgres:
|
||||
driver: local
|
||||
redis:
|
||||
driver: local
|
||||
minio-data:
|
||||
driver: local
|
||||
```
|
||||
|
||||
3. **Start services**
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
4. **Open the MinIO Console & Create a Bucket**
|
||||
|
||||
- Visit **http://localhost:9001**
|
||||
- Log in with:
|
||||
- **Username:** `formbricks-root`
|
||||
- **Password:** `change-this-secure-password`
|
||||
- Go to **Buckets → Create Bucket**
|
||||
- Name it: **`formbricks`**
|
||||
|
||||
5. **Configure Formbricks to use MinIO**
|
||||
|
||||
In your `.env` or `formbricks` service environment, set:
|
||||
|
||||
```bash
|
||||
# MinIO S3 Storage
|
||||
S3_ACCESS_KEY="formbricks-root"
|
||||
S3_SECRET_KEY="change-this-secure-password"
|
||||
S3_REGION="us-east-1"
|
||||
S3_BUCKET_NAME="formbricks"
|
||||
S3_ENDPOINT_URL="http://minio:9000"
|
||||
S3_FORCE_PATH_STYLE="1"
|
||||
```
|
||||
|
||||
<Note>
|
||||
These credentials should match <code>MINIO_ROOT_USER</code> and <code>MINIO_ROOT_PASSWORD</code> above.
|
||||
For local/dev this is fine. For production, create a dedicated MinIO user with restricted policies.
|
||||
</Note>
|
||||
|
||||
6. **Verify uploads**
|
||||
|
||||
After uploading a file in Formbricks, check **http://localhost:9001**:
|
||||
|
||||
- **Buckets → formbricks → Browse**
|
||||
You should see your uploaded files.
|
||||
|
||||
#### Tips & Common Gotchas
|
||||
|
||||
- **Connection refused**: Ensure the `minio` container is running and port **9000** is reachable from the Formbricks container (use the internal URL `http://minio:9000`).
|
||||
- **Bucket not found**: Create the `formbricks` bucket in the console before uploading.
|
||||
- **Auth failed**: Confirm `S3_ACCESS_KEY`/`S3_SECRET_KEY` match MinIO credentials.
|
||||
- **Health check**: From the Formbricks container:
|
||||
```bash
|
||||
docker compose exec formbricks sh -c 'wget -O- http://minio:9000/minio/health/ready'
|
||||
```
|
||||
|
||||
### Production Setup with Traefik
|
||||
|
||||
For production deployments, use the [one-click setup script](/self-hosting/setup/one-click) which automatically configures:
|
||||
|
||||
- MinIO service with Traefik reverse proxy
|
||||
- Dedicated subdomain (e.g., `files.yourdomain.com`) - **required for production**
|
||||
- Automatic SSL certificate generation via Let's Encrypt
|
||||
- CORS configuration for your domain
|
||||
- Rate limiting middleware
|
||||
- Secure credential generation
|
||||
|
||||
The production setup from [formbricks.sh](https://github.com/formbricks/formbricks/blob/main/docker/formbricks.sh) includes advanced features not covered in this manual setup. For production use, we strongly recommend using the one-click installer.
|
||||
|
||||
## Debug
|
||||
|
||||
If you encounter any issues, you can check the logs of the container with this command:
|
||||
|
||||
@@ -74,7 +74,7 @@ spec:
|
||||
{{- end }}
|
||||
{{- if .Values.deployment.securityContext }}
|
||||
securityContext:
|
||||
{{ toYaml .Values.deployment.securityContext | indent 8 }}
|
||||
{{ toYaml .Values.deployment.securityContext | nindent 8 }}
|
||||
{{- end }}
|
||||
terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds | default 30 }}
|
||||
containers:
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"next-auth@4.24.11": "patches/next-auth@4.24.11.patch"
|
||||
"next-auth@4.24.12": "patches/next-auth@4.24.12.patch"
|
||||
},
|
||||
"overrides": {
|
||||
"axios": ">=1.12.2",
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
*/
|
||||
|
||||
-- Update any scheduled surveys to paused
|
||||
UPDATE "public"."Survey" SET "status" = 'paused' WHERE "status" = 'scheduled';
|
||||
UPDATE "Survey" SET "status" = 'paused' WHERE "status" = 'scheduled';
|
||||
|
||||
-- AlterEnum
|
||||
BEGIN;
|
||||
CREATE TYPE "public"."SurveyStatus_new" AS ENUM ('draft', 'inProgress', 'paused', 'completed');
|
||||
ALTER TABLE "public"."Survey" ALTER COLUMN "status" DROP DEFAULT;
|
||||
ALTER TABLE "public"."Survey" ALTER COLUMN "status" TYPE "public"."SurveyStatus_new" USING ("status"::text::"public"."SurveyStatus_new");
|
||||
ALTER TYPE "public"."SurveyStatus" RENAME TO "SurveyStatus_old";
|
||||
ALTER TYPE "public"."SurveyStatus_new" RENAME TO "SurveyStatus";
|
||||
DROP TYPE "public"."SurveyStatus_old";
|
||||
ALTER TABLE "public"."Survey" ALTER COLUMN "status" SET DEFAULT 'draft';
|
||||
CREATE TYPE "SurveyStatus_new" AS ENUM ('draft', 'inProgress', 'paused', 'completed');
|
||||
ALTER TABLE "Survey" ALTER COLUMN "status" DROP DEFAULT;
|
||||
ALTER TABLE "Survey" ALTER COLUMN "status" TYPE "SurveyStatus_new" USING ("status"::text::"SurveyStatus_new");
|
||||
ALTER TYPE "SurveyStatus" RENAME TO "SurveyStatus_old";
|
||||
ALTER TYPE "SurveyStatus_new" RENAME TO "SurveyStatus";
|
||||
DROP TYPE "SurveyStatus_old";
|
||||
ALTER TABLE "Survey" ALTER COLUMN "status" SET DEFAULT 'draft';
|
||||
COMMIT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Survey" DROP COLUMN "closeOnDate",
|
||||
ALTER TABLE "Survey" DROP COLUMN "closeOnDate",
|
||||
DROP COLUMN "runOnDate";
|
||||
|
||||
@@ -20,88 +20,88 @@
|
||||
*/
|
||||
-- AlterEnum
|
||||
BEGIN;
|
||||
CREATE TYPE "public"."SurveyType_new" AS ENUM ('link', 'app');
|
||||
ALTER TABLE "public"."Survey" ALTER COLUMN "type" DROP DEFAULT;
|
||||
ALTER TABLE "public"."Survey" ALTER COLUMN "type" TYPE "public"."SurveyType_new" USING ("type"::text::"public"."SurveyType_new");
|
||||
ALTER TYPE "public"."SurveyType" RENAME TO "SurveyType_old";
|
||||
ALTER TYPE "public"."SurveyType_new" RENAME TO "SurveyType";
|
||||
DROP TYPE "public"."SurveyType_old";
|
||||
ALTER TABLE "public"."Survey" ALTER COLUMN "type" SET DEFAULT 'app';
|
||||
CREATE TYPE "SurveyType_new" AS ENUM ('link', 'app');
|
||||
ALTER TABLE "Survey" ALTER COLUMN "type" DROP DEFAULT;
|
||||
ALTER TABLE "Survey" ALTER COLUMN "type" TYPE "SurveyType_new" USING ("type"::text::"SurveyType_new");
|
||||
ALTER TYPE "SurveyType" RENAME TO "SurveyType_old";
|
||||
ALTER TYPE "SurveyType_new" RENAME TO "SurveyType";
|
||||
DROP TYPE "SurveyType_old";
|
||||
ALTER TABLE "Survey" ALTER COLUMN "type" SET DEFAULT 'app';
|
||||
COMMIT;
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_environmentId_fkey";
|
||||
ALTER TABLE "Document" DROP CONSTRAINT "Document_environmentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_responseId_fkey";
|
||||
ALTER TABLE "Document" DROP CONSTRAINT "Document_responseId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_surveyId_fkey";
|
||||
ALTER TABLE "Document" DROP CONSTRAINT "Document_surveyId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."DocumentInsight" DROP CONSTRAINT "DocumentInsight_documentId_fkey";
|
||||
ALTER TABLE "DocumentInsight" DROP CONSTRAINT "DocumentInsight_documentId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."DocumentInsight" DROP CONSTRAINT "DocumentInsight_insightId_fkey";
|
||||
ALTER TABLE "DocumentInsight" DROP CONSTRAINT "DocumentInsight_insightId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "public"."Insight" DROP CONSTRAINT "Insight_environmentId_fkey";
|
||||
ALTER TABLE "Insight" DROP CONSTRAINT "Insight_environmentId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Display_responseId_key";
|
||||
DROP INDEX "Display_responseId_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Display" DROP COLUMN "responseId",
|
||||
ALTER TABLE "Display" DROP COLUMN "responseId",
|
||||
DROP COLUMN "status";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Environment" DROP COLUMN "widgetSetupCompleted";
|
||||
ALTER TABLE "Environment" DROP COLUMN "widgetSetupCompleted";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Invite" DROP COLUMN "deprecatedRole";
|
||||
ALTER TABLE "Invite" DROP COLUMN "deprecatedRole";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Membership" DROP COLUMN "deprecatedRole";
|
||||
ALTER TABLE "Membership" DROP COLUMN "deprecatedRole";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Project" DROP COLUMN "brandColor",
|
||||
ALTER TABLE "Project" DROP COLUMN "brandColor",
|
||||
DROP COLUMN "highlightBorderColor";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Survey" DROP COLUMN "thankYouCard",
|
||||
ALTER TABLE "Survey" DROP COLUMN "thankYouCard",
|
||||
DROP COLUMN "verifyEmail",
|
||||
ALTER COLUMN "type" SET DEFAULT 'app';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."User" DROP COLUMN "objective",
|
||||
ALTER TABLE "User" DROP COLUMN "objective",
|
||||
DROP COLUMN "role";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "public"."Document";
|
||||
DROP TABLE "Document";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "public"."DocumentInsight";
|
||||
DROP TABLE "DocumentInsight";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "public"."Insight";
|
||||
DROP TABLE "Insight";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."DisplayStatus";
|
||||
DROP TYPE "DisplayStatus";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."InsightCategory";
|
||||
DROP TYPE "InsightCategory";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."Intention";
|
||||
DROP TYPE "Intention";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."MembershipRole";
|
||||
DROP TYPE "MembershipRole";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."Objective";
|
||||
DROP TYPE "Objective";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."Role";
|
||||
DROP TYPE "Role";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "public"."Sentiment";
|
||||
DROP TYPE "Sentiment";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
-- DropIndex
|
||||
DROP INDEX IF EXISTS "public"."ApiKey_hashedKey_key";
|
||||
DROP INDEX IF EXISTS "ApiKey_hashedKey_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."ApiKey" ADD COLUMN IF NOT EXISTS "lookupHash" TEXT;
|
||||
ALTER TABLE "ApiKey" ADD COLUMN IF NOT EXISTS "lookupHash" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "ApiKey_lookupHash_key" ON "public"."ApiKey"("lookupHash");
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "ApiKey_lookupHash_key" ON "ApiKey"("lookupHash");
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"locale": {
|
||||
"source": "en",
|
||||
"targets": ["de", "it", "fr", "es", "ar", "pt", "ru", "uz", "ro", "ja", "zh-Hans", "hi"]
|
||||
"targets": ["de", "it", "fr", "es", "ar", "pt", "ru", "uz", "ro", "ja", "zh-Hans", "hi", "nl"]
|
||||
},
|
||||
"version": 1.8
|
||||
}
|
||||
|
||||
73
packages/surveys/locales/nl.json
Normal file
73
packages/surveys/locales/nl.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"common": {
|
||||
"and": "en",
|
||||
"apply": "Toepassen",
|
||||
"auto_close_wrapper": "Automatisch sluitende wrapper",
|
||||
"back": "Terug",
|
||||
"click_or_drag_to_upload_files": "Klik of sleep om bestanden te uploaden.",
|
||||
"close_survey": "Enquête sluiten",
|
||||
"company_logo": "Bedrijfslogo",
|
||||
"delete_file": "Bestand verwijderen",
|
||||
"file_upload": "Bestand uploaden",
|
||||
"finish": "Voltooien",
|
||||
"language_switch": "Taalschakelaar",
|
||||
"less_than_x_minutes": "{count, plural, one {minder dan 1 minuut} other {minder dan {count} minuten}}",
|
||||
"move_down": "Verplaats {item} naar beneden",
|
||||
"move_up": "Verplaats {item} omhoog",
|
||||
"next": "Volgende",
|
||||
"open_in_new_tab": "Openen in nieuw tabblad",
|
||||
"optional": "Optioneel",
|
||||
"options": "Opties",
|
||||
"people_responded": "{count, plural, one {1 persoon heeft gereageerd} other {{count} mensen hebben gereageerd}}",
|
||||
"please_retry_now_or_try_again_later": "Probeer het nu opnieuw of probeer het later opnieuw.",
|
||||
"powered_by": "Aangedreven door",
|
||||
"privacy_policy": "Privacybeleid",
|
||||
"protected_by_reCAPTCHA_and_the_Google": "Beschermd door reCAPTCHA en Google",
|
||||
"question": "Vraag",
|
||||
"question_video": "Vraagvideo",
|
||||
"ranking_items": "Items rangschikken",
|
||||
"respondents_will_not_see_this_card": "Respondenten zien deze kaart niet",
|
||||
"retry": "Opnieuw proberen",
|
||||
"select_a_date": "Selecteer een datum",
|
||||
"select_for_ranking": "Selecteer {item} voor rangschikking",
|
||||
"sending_responses": "Reacties verzenden...",
|
||||
"takes": "Neemt",
|
||||
"terms_of_service": "Servicevoorwaarden",
|
||||
"the_servers_cannot_be_reached_at_the_moment": "De servers zijn momenteel niet bereikbaar.",
|
||||
"they_will_be_redirected_immediately": "Ze worden onmiddellijk doorgestuurd",
|
||||
"upload_files_by_clicking_or_dragging_them_here": "Upload bestanden door ze hierheen te klikken of te slepen",
|
||||
"uploading": "Uploaden",
|
||||
"x_minutes": "{count, plural, one {1 minuut} other {{count} minuten}}",
|
||||
"x_plus_minutes": "{count}+ minuten",
|
||||
"you_have_selected_x_date": "Je hebt {date} geselecteerd",
|
||||
"you_have_successfully_uploaded_the_file": "Je hebt het bestand {fileName} succesvol geüpload",
|
||||
"your_feedback_is_stuck": "Je feedback blijft hangen :("
|
||||
},
|
||||
"errors": {
|
||||
"file_input": {
|
||||
"duplicate_files": "De volgende bestanden zijn al geüpload: {duplicateNames}. Dubbele bestanden zijn niet toegestaan.",
|
||||
"file_size_exceeded": "De volgende bestanden overschrijden de maximale grootte van {maxSizeInMB} MB en zijn verwijderd: {fileNames}",
|
||||
"file_size_exceeded_alert": "Het bestand moet kleiner zijn dan {maxSizeInMB} MB",
|
||||
"no_valid_file_types_selected": "Geen geldige bestandstypen geselecteerd. Selecteer een geldig bestandstype.",
|
||||
"only_one_file_can_be_uploaded_at_a_time": "Er kan slechts één bestand tegelijk worden geüpload.",
|
||||
"upload_failed": "Uploaden mislukt! Probeer het opnieuw.",
|
||||
"you_can_only_upload_a_maximum_of_files": "Je kunt maximaal {FILE_LIMIT} bestanden uploaden."
|
||||
},
|
||||
"invalid_device_error": {
|
||||
"message": "Schakel de spambeveiliging uit in de enquête-instellingen om dit apparaat te blijven gebruiken.",
|
||||
"title": "Dit apparaat ondersteunt geen spambeveiliging."
|
||||
},
|
||||
"please_book_an_appointment": "Maak een afspraak",
|
||||
"please_enter_a_valid_email_address": "Voer een geldig e-mailadres in",
|
||||
"please_enter_a_valid_phone_number": "Voer een geldig telefoonnummer in",
|
||||
"please_enter_a_valid_url": "Voer een geldige URL in",
|
||||
"please_fill_out_this_field": "Vul dit veld in",
|
||||
"please_rank_all_items_before_submitting": "Rangschik alle items voordat u ze verzendt",
|
||||
"please_select_a_date": "Selecteer een datum",
|
||||
"please_upload_a_file": "Upload een bestand",
|
||||
"recaptcha_error": {
|
||||
"message": "Uw reactie kan niet worden verzonden omdat deze is gemarkeerd als geautomatiseerde activiteit. Als u ademhaalt, probeer het dan opnieuw.",
|
||||
"title": "We konden niet verifiëren dat je een mens bent."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import frTranslations from "../../locales/fr.json";
|
||||
import hiTranslations from "../../locales/hi.json";
|
||||
import itTranslations from "../../locales/it.json";
|
||||
import jaTranslations from "../../locales/ja.json";
|
||||
import nlTranslations from "../../locales/nl.json";
|
||||
import ptTranslations from "../../locales/pt.json";
|
||||
import roTranslations from "../../locales/ro.json";
|
||||
import ruTranslations from "../../locales/ru.json";
|
||||
@@ -20,7 +21,7 @@ i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
supportedLngs: ["en", "de", "it", "fr", "es", "ar", "pt", "ro", "ja", "ru", "uz", "zh-Hans", "hi"],
|
||||
supportedLngs: ["en", "de", "it", "fr", "es", "ar", "pt", "ro", "ja", "ru", "uz", "zh-Hans", "hi", "nl"],
|
||||
|
||||
resources: {
|
||||
en: { translation: enTranslations },
|
||||
@@ -32,6 +33,7 @@ i18n
|
||||
pt: { translation: ptTranslations },
|
||||
ro: { translation: roTranslations },
|
||||
ja: { translation: jaTranslations },
|
||||
nl: { translation: nlTranslations },
|
||||
ru: { translation: ruTranslations },
|
||||
uz: { translation: uzTranslations },
|
||||
"zh-Hans": { translation: zhHansTranslations },
|
||||
|
||||
@@ -5,6 +5,7 @@ export const ZUserLocale = z.enum([
|
||||
"de-DE",
|
||||
"pt-BR",
|
||||
"fr-FR",
|
||||
"nl-NL",
|
||||
"zh-Hant-TW",
|
||||
"pt-PT",
|
||||
"ro-RO",
|
||||
|
||||
5351
pnpm-lock.yaml
generated
5351
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user