diff --git a/.cursor/rules/database.mdc b/.cursor/rules/database.mdc index 4de6ceb10f..beee6adfa1 100644 --- a/.cursor/rules/database.mdc +++ b/.cursor/rules/database.mdc @@ -1,6 +1,11 @@ --- -description: It should be used **only when the agent explicitly requests database schema-level, details** to support tasks such as: writing/debugging Prisma queries, designing/reviewing data models, investigating multi-tenancy behavior, creating API endpoints, or understanding data relationships. -alwaysApply: false +description: > + This rule provides comprehensive knowledge about the Formbricks database structure, relationships, + and data patterns. It should be used **only when the agent explicitly requests database schema-level + details** to support tasks such as: writing/debugging Prisma queries, designing/reviewing data models, + investigating multi-tenancy behavior, creating API endpoints, or understanding data relationships. +globs: [] +alwaysApply: agent-requested --- # Formbricks Database Schema Reference diff --git a/.cursor/rules/overview.mdc b/.cursor/rules/overview.mdc deleted file mode 100644 index 0f2480b59d..0000000000 --- a/.cursor/rules/overview.mdc +++ /dev/null @@ -1,74 +0,0 @@ ---- -alwaysApply: true ---- - -### Formbricks Monorepo Overview - -- **Project**: Formbricks — open‑source survey and experience management platform. Repo: [formbricks/formbricks](https://github.com/formbricks/formbricks) -- **Monorepo**: Turborepo + pnpm workspaces. Root configs: [package.json](mdc:package.json), [turbo.json](mdc:turbo.json) -- **Core app**: Next.js app in `apps/web` with Prisma, Auth.js, TailwindCSS, Vitest, Playwright. Enterprise modules live in [apps/web/modules/ee](mdc:apps/web/modules/ee) -- **Datastores**: PostgreSQL + Redis. Local dev via [docker-compose.dev.yml](mdc:docker-compose.dev.yml); Prisma schema at [packages/database/schema.prisma](mdc:packages/database/schema.prisma) -- **Docs & Ops**: Docs in `docs/` (Mintlify), Helm in `helm-chart/`, IaC in `infra/` - -### Apps - -- **apps/web**: Next.js product application (API, UI, SSO, i18n, emails, uploads, integrations) -- **apps/storybook**: Storybook for UI components; a11y addon + Vite builder - -### Packages - -- **@formbricks/database** (`packages/database`): Prisma schema, DB scripts, migrations, data layer -- **@formbricks/js-core** (`packages/js-core`): Core runtime for web embed / async loader -- **@formbricks/surveys** (`packages/surveys`): Embeddable survey rendering and helpers -- **@formbricks/logger** (`packages/logger`): Shared logging (pino) + Zod types -- **@formbricks/types** (`packages/types`): Shared types (Zod, Prisma clients) -- **@formbricks/i18n-utils** (`packages/i18n-utils`): i18n helpers and build output -- **@formbricks/eslint-config** (`packages/config-eslint`): Central ESLint config (Next, TS, Vitest, Prettier) -- **@formbricks/config-typescript** (`packages/config-typescript`): Central TS config and types -- **@formbricks/vite-plugins** (`packages/vite-plugins`): Internal Vite plugins -- **packages/android, packages/ios**: Native SDKs (built with platform toolchains) - -### Enterprise‑ready by design - -- **Quality & safety**: Strict TypeScript, repo‑wide ESLint + Prettier, lint‑staged + Husky, CI checks, typed env validation -- **Security‑first**: Auth.js, SSO/SAML/OIDC, session controls, rate limiting, Sentry, structured logging - -### Accessible by design - -- **UI foundations**: Radix UI, TailwindCSS, Storybook with `@storybook/addon-a11y`, keyboard and screen‑reader‑friendly components - -### Root pnpm commands - -```bash -pnpm clean:all # Clean turbo cache, node_modules, lockfile, coverage, out -pnpm clean # Clean turbo cache, node_modules, coverage, out -pnpm build # Build all packages/apps (turbo) -pnpm build:dev # Dev-optimized builds (where supported) -pnpm dev # Run all dev servers in parallel -pnpm start # Start built apps/services -pnpm go # Start DB (docker compose) and run long-running dev tasks -pnpm generate # Run generators (e.g., Prisma, API specs) -pnpm lint # Lint all -pnpm format # Prettier write across repo -pnpm test # Unit tests -pnpm test:coverage # Unit tests with coverage -pnpm test:e2e # Playwright tests -pnpm test-e2e:azure # Playwright tests with Azure config -pnpm storybook # Run Storybook -pnpm db:up # Start local Postgres/Redis via docker compose -pnpm db:down # Stop local DB stack -pnpm db:start # Project-level DB setup choreography -pnpm db:push # Prisma db push (accept data loss in package script) -pnpm db:migrate:dev # Apply dev migrations -pnpm db:migrate:deploy # Apply prod migrations -pnpm fb-migrate-dev # Create DB migration (database package) and prisma generate -pnpm tolgee-pull # Pull translation keys for current branch and format -``` - -### Essentials for every prompt - -- **Tech stack**: Next.js, React 19, TypeScript, Prisma, Zod, TailwindCSS, Turborepo, Vitest, Playwright -- **Environments**: See `.env.example`. Many tasks require DB up and env variables set -- **Licensing**: Core under AGPLv3; Enterprise code in `apps/web/modules/ee` (included in Docker, unlocked via Enterprise License Key) - -For deeper details, consult per‑package `package.json` and scripts (e.g., [apps/web/package.json](mdc:apps/web/package.json)). diff --git a/apps/web/modules/auth/components/form-wrapper.tsx b/apps/web/modules/auth/components/form-wrapper.tsx index 8b02d84b58..0439d8f96d 100644 --- a/apps/web/modules/auth/components/form-wrapper.tsx +++ b/apps/web/modules/auth/components/form-wrapper.tsx @@ -10,12 +10,8 @@ export const FormWrapper = ({ children }: FormWrapperProps) => {
- -
{children} diff --git a/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.test.tsx b/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.test.tsx index 26c6d23156..2849a61a48 100644 --- a/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.test.tsx +++ b/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.test.tsx @@ -149,10 +149,10 @@ describe("AddApiKeyModal", () => { test("handles label input", async () => { render(); - const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack"); + const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack") as HTMLInputElement; await userEvent.type(labelInput, "Test API Key"); - expect((labelInput as HTMLInputElement).value).toBe("Test API Key"); + expect(labelInput.value).toBe("Test API Key"); }); test("handles permission changes", async () => { @@ -184,120 +184,21 @@ describe("AddApiKeyModal", () => { await userEvent.click(addButton); // Verify new permission row is added - const deleteButtons = await screen.findAllByRole("button", { - name: "environments.project.api_keys.delete_permission", - }); + const deleteButtons = screen.getAllByRole("button", { name: "" }); // Trash icons expect(deleteButtons).toHaveLength(2); // Remove the new permission await userEvent.click(deleteButtons[1]); // Check that only the original permission row remains - const remainingDeleteButtons = await screen.findAllByRole("button", { - name: "environments.project.api_keys.delete_permission", - }); - expect(remainingDeleteButtons).toHaveLength(1); - }); - - test("removes permissions from middle of list without breaking indices", async () => { - render(); - - // Add first permission - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - // Add second permission - await userEvent.click(addButton); - - // Add third permission - await userEvent.click(addButton); - - // Verify we have 3 permission rows - let deleteButtons = await screen.findAllByRole("button", { - name: "environments.project.api_keys.delete_permission", - }); - expect(deleteButtons).toHaveLength(3); - - // Remove the middle permission (index 1) - await userEvent.click(deleteButtons[1]); - - // Verify we now have 2 permission rows - deleteButtons = await screen.findAllByRole("button", { - name: "environments.project.api_keys.delete_permission", - }); - expect(deleteButtons).toHaveLength(2); - - // Try to remove the second remaining permission (this was previously index 2, now index 1) - await userEvent.click(deleteButtons[1]); - - // Verify we now have 1 permission row - deleteButtons = await screen.findAllByRole("button", { - name: "environments.project.api_keys.delete_permission", - }); - expect(deleteButtons).toHaveLength(1); - - // Remove the last remaining permission - await userEvent.click(deleteButtons[0]); - - // Verify no permission rows remain - expect( - screen.queryAllByRole("button", { name: "environments.project.api_keys.delete_permission" }) - ).toHaveLength(0); - }); - - test("can modify permissions after deleting items from list", async () => { - render(); - - // Add multiple permissions - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); // First permission - await userEvent.click(addButton); // Second permission - await userEvent.click(addButton); // Third permission - - // Verify we have 3 permission rows - let deleteButtons = await screen.findAllByRole("button", { - name: "environments.project.api_keys.delete_permission", - }); - expect(deleteButtons).toHaveLength(3); - - // Remove the first permission (index 0) - await userEvent.click(deleteButtons[0]); - - // Verify we now have 2 permission rows - deleteButtons = await screen.findAllByRole("button", { - name: "environments.project.api_keys.delete_permission", - }); - expect(deleteButtons).toHaveLength(2); - - // Try to modify the first remaining permission (which was originally index 1, now index 0) - const projectDropdowns = screen.getAllByRole("button", { name: /Project 1/i }); - expect(projectDropdowns.length).toBeGreaterThan(0); - - await userEvent.click(projectDropdowns[0]); - - // Wait for dropdown content and select 'Project 2' - const project2Option = await screen.findByRole("menuitem", { name: "Project 2" }); - await userEvent.click(project2Option); - - // Verify project selection by checking the updated button text - const updatedButton = await screen.findByRole("button", { name: "Project 2" }); - expect(updatedButton).toBeInTheDocument(); - - // Add another permission to verify the list is still functional - await userEvent.click(addButton); - - // Verify we now have 3 permission rows again - deleteButtons = await screen.findAllByRole("button", { - name: "environments.project.api_keys.delete_permission", - }); - expect(deleteButtons).toHaveLength(3); + expect(screen.getAllByRole("button", { name: "" })).toHaveLength(1); }); test("submits form with correct data", async () => { render(); // Fill in label - const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack"); + const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack") as HTMLInputElement; await userEvent.type(labelInput, "Test API Key"); const addButton = screen.getByRole("button", { name: /add_permission/i }); @@ -377,7 +278,7 @@ describe("AddApiKeyModal", () => { render(); // Type something into the label - const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack"); + const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack") as HTMLInputElement; await userEvent.type(labelInput, "Test API Key"); // Click the cancel button @@ -386,219 +287,6 @@ describe("AddApiKeyModal", () => { // Verify modal is closed and form is reset expect(mockSetOpen).toHaveBeenCalledWith(false); - expect((labelInput as HTMLInputElement).value).toBe(""); - }); - - test("updates permission field (non-environmentId)", async () => { - render(); - - // Add a permission first - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - // Click on permission level dropdown (third dropdown in the row) - const permissionDropdowns = screen.getAllByRole("button", { name: /read/i }); - await userEvent.click(permissionDropdowns[0]); - - // Select 'write' permission - const writeOption = await screen.findByRole("menuitem", { name: "write" }); - await userEvent.click(writeOption); - - // Verify permission selection by checking the updated button text - const updatedButton = await screen.findByRole("button", { name: "write" }); - expect(updatedButton).toBeInTheDocument(); - }); - - test("updates environmentId with valid environment", async () => { - render(); - - // Add a permission first - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - // Click on environment dropdown (second dropdown in the row) - const environmentDropdowns = screen.getAllByRole("button", { name: /production/i }); - await userEvent.click(environmentDropdowns[0]); - - // Select 'development' environment - const developmentOption = await screen.findByRole("menuitem", { name: "development" }); - await userEvent.click(developmentOption); - - // Verify environment selection by checking the updated button text - const updatedButton = await screen.findByRole("button", { name: "development" }); - expect(updatedButton).toBeInTheDocument(); - }); - - test("updates project and automatically selects first environment", async () => { - render(); - - // Add a permission first - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - // Initially should show Project 1 and production environment - expect(screen.getByRole("button", { name: "Project 1" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: /production/i })).toBeInTheDocument(); - - // Click on project dropdown (first dropdown in the row) - const projectDropdowns = screen.getAllByRole("button", { name: /Project 1/i }); - await userEvent.click(projectDropdowns[0]); - - // Select 'Project 2' - const project2Option = await screen.findByRole("menuitem", { name: "Project 2" }); - await userEvent.click(project2Option); - - // Verify project selection and that environment was auto-updated - const updatedProjectButton = await screen.findByRole("button", { name: "Project 2" }); - expect(updatedProjectButton).toBeInTheDocument(); - - // Environment should still be production (first environment of Project 2) - expect(screen.getByRole("button", { name: /production/i })).toBeInTheDocument(); - }); - - test("handles edge case when project is not found", async () => { - // Create a modified mock with corrupted project reference - const corruptedProjects = [ - { - ...mockProjects[0], - id: "different-id", // This will cause project lookup to fail - }, - ]; - - render(); - - // Add a permission first - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - // The component should still render without crashing - expect(screen.getByRole("button", { name: /add_permission/i })).toBeInTheDocument(); - - // Try to interact with environment dropdown - should not crash - const environmentDropdowns = screen.getAllByRole("button", { name: /production/i }); - await userEvent.click(environmentDropdowns[0]); - - // Should be able to find and click on development option - const developmentOption = await screen.findByRole("menuitem", { name: "development" }); - await userEvent.click(developmentOption); - - // Verify environment selection works even when project lookup fails - const updatedButton = await screen.findByRole("button", { name: "development" }); - expect(updatedButton).toBeInTheDocument(); - }); - - test("handles edge case when environment is not found", async () => { - // Create a project with no environments - const projectWithNoEnvs = [ - { - ...mockProjects[0], - environments: [], // No environments available - }, - ]; - - render(); - - // Try to add a permission - this should handle the case gracefully - const addButton = screen.getByRole("button", { name: /add_permission/i }); - - // This might not add a permission if no environments exist, which is expected behavior - await userEvent.click(addButton); - - // Component should still be functional - expect(screen.getByRole("button", { name: /add_permission/i })).toBeInTheDocument(); - }); - - test("validates duplicate permissions detection", async () => { - render(); - - // Fill in a label - const labelInput = screen.getByPlaceholderText("e.g. GitHub, PostHog, Slack"); - await userEvent.type(labelInput, "Test API Key"); - - // Add first permission - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - // Add second permission with same project/environment - await userEvent.click(addButton); - - // Both permissions should now have the same project and environment (Project 1, production) - // Try to submit the form - it should show duplicate error - const submitButton = screen.getByRole("button", { - name: "environments.project.api_keys.add_api_key", - }); - await userEvent.click(submitButton); - - // The submit should not have been called due to duplicate detection - expect(mockOnSubmit).not.toHaveBeenCalled(); - }); - - test("handles updatePermission with environmentId but environment not found", async () => { - // Create a project with limited environments to test the edge case - const limitedProjects = [ - { - ...mockProjects[0], - environments: [ - { - id: "env1", - type: "production" as const, - createdAt: new Date(), - updatedAt: new Date(), - projectId: "project1", - appSetupCompleted: true, - }, - // Only one environment, so we can test when trying to update to non-existent env - ], - }, - ]; - - render(); - - // Add a permission first - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - // Verify permission was added with production environment - expect(screen.getByRole("button", { name: /production/i })).toBeInTheDocument(); - - // Now test the edge case by manually calling the component's internal logic - // Since we can't directly access the updatePermission function in tests, - // we test through the UI interactions and verify the component doesn't crash - - // The component should handle gracefully when environment lookup fails - // This tests the branch: field === "environmentId" && !environment - expect(screen.getByRole("button", { name: /production/i })).toBeInTheDocument(); - }); - - test("covers all branches of updatePermission function", async () => { - render(); - - // Add a permission to have something to update - const addButton = screen.getByRole("button", { name: /add_permission/i }); - await userEvent.click(addButton); - - // Test Branch 1: Update non-environmentId field (permission level) - const permissionDropdowns = screen.getAllByRole("button", { name: /read/i }); - await userEvent.click(permissionDropdowns[0]); - const manageOption = await screen.findByRole("menuitem", { name: "manage" }); - await userEvent.click(manageOption); - expect(await screen.findByRole("button", { name: "manage" })).toBeInTheDocument(); - - // Test Branch 2: Update environmentId with valid environment - const environmentDropdowns = screen.getAllByRole("button", { name: /production/i }); - await userEvent.click(environmentDropdowns[0]); - const developmentOption = await screen.findByRole("menuitem", { name: "development" }); - await userEvent.click(developmentOption); - expect(await screen.findByRole("button", { name: "development" })).toBeInTheDocument(); - - // Test Branch 3: Update project (which calls updateProjectAndEnvironment) - const projectDropdowns = screen.getAllByRole("button", { name: /Project 1/i }); - await userEvent.click(projectDropdowns[0]); - const project2Option = await screen.findByRole("menuitem", { name: "Project 2" }); - await userEvent.click(project2Option); - expect(await screen.findByRole("button", { name: "Project 2" })).toBeInTheDocument(); - - // Verify all updates worked correctly and component is still functional - expect(screen.getByRole("button", { name: /add_permission/i })).toBeInTheDocument(); + expect(labelInput.value).toBe(""); }); }); diff --git a/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.tsx b/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.tsx index e6f86c5afe..a3a0264660 100644 --- a/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.tsx +++ b/apps/web/modules/organization/settings/api-keys/components/add-api-key-modal.tsx @@ -80,22 +80,23 @@ export const AddApiKeyModal = ({ const [selectedOrganizationAccess, setSelectedOrganizationAccess] = useState(defaultOrganizationAccess); - const getInitialPermissions = (): PermissionRecord[] => { + const getInitialPermissions = () => { if (projects.length > 0 && projects[0].environments.length > 0) { - return [ - { + return { + "permission-0": { projectId: projects[0].id, environmentId: projects[0].environments[0].id, permission: ApiKeyPermission.read, projectName: projects[0].name, environmentType: projects[0].environments[0].type, }, - ]; + }; } - return []; + return {} as Record; }; - const [selectedPermissions, setSelectedPermissions] = useState([]); + // Initialize with one permission by default + const [selectedPermissions, setSelectedPermissions] = useState>({}); const projectOptions: ProjectOption[] = projects.map((project) => ({ id: project.id, @@ -103,54 +104,58 @@ export const AddApiKeyModal = ({ })); const removePermission = (index: number) => { - const updatedPermissions = [...selectedPermissions]; - updatedPermissions.splice(index, 1); + const updatedPermissions = { ...selectedPermissions }; + delete updatedPermissions[`permission-${index}`]; setSelectedPermissions(updatedPermissions); }; const addPermission = () => { - const initialPermissions = getInitialPermissions(); - if (initialPermissions.length > 0) { - setSelectedPermissions([...selectedPermissions, initialPermissions[0]]); + const newIndex = Object.keys(selectedPermissions).length; + const initialPermission = getInitialPermissions()["permission-0"]; + if (initialPermission) { + setSelectedPermissions({ + ...selectedPermissions, + [`permission-${newIndex}`]: initialPermission, + }); } }; - const updatePermission = (index: number, field: string, value: string) => { - const updatedPermissions = [...selectedPermissions]; - const project = projects.find((p) => p.id === updatedPermissions[index].projectId); + const updatePermission = (key: string, field: string, value: string) => { + const project = projects.find((p) => p.id === selectedPermissions[key].projectId); const environment = project?.environments.find((env) => env.id === value); - updatedPermissions[index] = { - ...updatedPermissions[index], - [field]: value, - ...(field === "environmentId" && environment ? { environmentType: environment.type } : {}), - }; - - setSelectedPermissions(updatedPermissions); + setSelectedPermissions({ + ...selectedPermissions, + [key]: { + ...selectedPermissions[key], + [field]: value, + ...(field === "environmentId" && environment ? { environmentType: environment.type } : {}), + }, + }); }; // Update environment when project changes - const updateProjectAndEnvironment = (index: number, projectId: string) => { + const updateProjectAndEnvironment = (key: string, projectId: string) => { const project = projects.find((p) => p.id === projectId); if (project && project.environments.length > 0) { const environment = project.environments[0]; - const updatedPermissions = [...selectedPermissions]; - - updatedPermissions[index] = { - ...updatedPermissions[index], - projectId, - environmentId: environment.id, - projectName: project.name, - environmentType: environment.type, - }; - - setSelectedPermissions(updatedPermissions); + setSelectedPermissions({ + ...selectedPermissions, + [key]: { + ...selectedPermissions[key], + projectId, + environmentId: environment.id, + projectName: project.name, + environmentType: environment.type, + }, + }); } }; const checkForDuplicatePermissions = () => { - const uniquePermissions = new Set(selectedPermissions.map((p) => `${p.projectId}-${p.environmentId}`)); - return uniquePermissions.size !== selectedPermissions.length; + const permissions = Object.values(selectedPermissions); + const uniquePermissions = new Set(permissions.map((p) => `${p.projectId}-${p.environmentId}`)); + return uniquePermissions.size !== permissions.length; }; const submitAPIKey = async () => { @@ -162,7 +167,7 @@ export const AddApiKeyModal = ({ } // Convert permissions to the format expected by the API - const environmentPermissions = selectedPermissions.map((permission) => ({ + const environmentPermissions = Object.values(selectedPermissions).map((permission) => ({ environmentId: permission.environmentId, permission: permission.permission, })); @@ -174,7 +179,7 @@ export const AddApiKeyModal = ({ }); reset(); - setSelectedPermissions([]); + setSelectedPermissions({}); setSelectedOrganizationAccess(defaultOrganizationAccess); }; @@ -191,7 +196,7 @@ export const AddApiKeyModal = ({ } // Check if at least one project permission is set or one organization access toggle is ON - const hasProjectAccess = selectedPermissions.length > 0; + const hasProjectAccess = Object.keys(selectedPermissions).length > 0; const hasOrganizationAccess = Object.values(selectedOrganizationAccess).some((accessGroup) => Object.values(accessGroup).some((value) => value === true) @@ -230,9 +235,13 @@ export const AddApiKeyModal = ({
- {selectedPermissions.map((permission, index) => { + {/* Permission rows */} + {Object.keys(selectedPermissions).map((key) => { + const permissionIndex = parseInt(key.split("-")[1]); + const permission = selectedPermissions[key]; return ( -
+
+ {/* Project dropdown */}
@@ -252,7 +261,7 @@ export const AddApiKeyModal = ({ { - updateProjectAndEnvironment(index, option.id); + updateProjectAndEnvironment(key, option.id); }}> {option.name} @@ -260,6 +269,8 @@ export const AddApiKeyModal = ({
+ + {/* Environment dropdown */}
@@ -281,7 +292,7 @@ export const AddApiKeyModal = ({ { - updatePermission(index, "environmentId", env.id); + updatePermission(key, "environmentId", env.id); }}> {env.type} @@ -289,6 +300,8 @@ export const AddApiKeyModal = ({
+ + {/* Permission level dropdown */}
@@ -310,7 +323,7 @@ export const AddApiKeyModal = ({ { - updatePermission(index, "permission", option); + updatePermission(key, "permission", option); }}> {option} @@ -318,16 +331,16 @@ export const AddApiKeyModal = ({
-
); })} + + {/* Add permission button */} @@ -384,7 +397,7 @@ export const AddApiKeyModal = ({ onClick={() => { setOpen(false); reset(); - setSelectedPermissions([]); + setSelectedPermissions({}); }}> {t("common.cancel")} diff --git a/apps/web/modules/setup/layout.tsx b/apps/web/modules/setup/layout.tsx index 616c37a8d8..2e4722ec5f 100644 --- a/apps/web/modules/setup/layout.tsx +++ b/apps/web/modules/setup/layout.tsx @@ -1,4 +1,4 @@ -import { Logo } from "@/modules/ui/components/logo"; +import { FormbricksLogo } from "@/modules/ui/components/formbricks-logo"; import { Toaster } from "react-hot-toast"; export const SetupLayout = ({ children }: { children: React.ReactNode }) => { @@ -10,7 +10,7 @@ export const SetupLayout = ({ children }: { children: React.ReactNode }) => { style={{ scrollbarGutter: "stable both-edges" }} className="flex max-h-[90vh] w-[40rem] flex-col items-center space-y-4 overflow-auto rounded-lg border bg-white p-12 text-center shadow-md">
- +
{children}
diff --git a/apps/web/modules/ui/components/connect-integration/index.test.tsx b/apps/web/modules/ui/components/connect-integration/index.test.tsx index c063340115..e5a3edbbad 100644 --- a/apps/web/modules/ui/components/connect-integration/index.test.tsx +++ b/apps/web/modules/ui/components/connect-integration/index.test.tsx @@ -41,8 +41,8 @@ vi.mock("next/link", () => ({ ), })); -vi.mock("@/modules/ui/components/logo", () => ({ - Logo: () =>
Logo
, +vi.mock("@/modules/ui/components/formbricks-logo", () => ({ + FormbricksLogo: () =>
FormbricksLogo
, })); vi.mock("@/modules/ui/components/button", () => ({ diff --git a/apps/web/modules/ui/components/connect-integration/index.tsx b/apps/web/modules/ui/components/connect-integration/index.tsx index 67540bcd6d..984aba3221 100644 --- a/apps/web/modules/ui/components/connect-integration/index.tsx +++ b/apps/web/modules/ui/components/connect-integration/index.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/modules/ui/components/button"; -import { Logo } from "@/modules/ui/components/logo"; +import { FormbricksLogo } from "@/modules/ui/components/formbricks-logo"; import { useTranslate } from "@tolgee/react"; import Image, { StaticImageData } from "next/image"; import Link from "next/link"; @@ -51,7 +51,7 @@ export const ConnectIntegration = ({
- +
logo diff --git a/apps/web/modules/ui/components/formbricks-logo/index.tsx b/apps/web/modules/ui/components/formbricks-logo/index.tsx new file mode 100644 index 0000000000..7b2d66e54c --- /dev/null +++ b/apps/web/modules/ui/components/formbricks-logo/index.tsx @@ -0,0 +1,197 @@ +interface FormbricksLogoProps { + className?: string; +} + +export const FormbricksLogo = ({ className }: FormbricksLogoProps) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/web/modules/ui/components/logo/index.test.tsx b/apps/web/modules/ui/components/logo/index.test.tsx index 4adea7f2e8..cae4bb4dc2 100644 --- a/apps/web/modules/ui/components/logo/index.test.tsx +++ b/apps/web/modules/ui/components/logo/index.test.tsx @@ -8,59 +8,33 @@ describe("Logo", () => { cleanup(); }); - describe("default variant", () => { - test("renders default logo correctly", () => { - const { container } = render(); - const svg = container.querySelector("svg"); + test("renders correctly", () => { + const { container } = render(); + const svg = container.querySelector("svg"); - expect(svg).toBeInTheDocument(); - }); + expect(svg).toBeInTheDocument(); + expect(svg).toHaveAttribute("viewBox", "0 0 697 150"); + expect(svg).toHaveAttribute("fill", "none"); + expect(svg).toHaveAttribute("xmlns", "http://www.w3.org/2000/svg"); }); - describe("image variant", () => { - test("renders image logo correctly", () => { - const { container } = render(); - const svg = container.querySelector("svg"); + test("accepts and passes through props", () => { + const testClassName = "test-class"; + const { container } = render(); + const svg = container.querySelector("svg"); - expect(svg).toBeInTheDocument(); - }); - - test("renders image logo with className correctly", () => { - const testClassName = "test-class"; - const { container } = render(); - const svg = container.querySelector("svg"); - - expect(svg).toBeInTheDocument(); - expect(svg).toHaveAttribute("class", testClassName); - }); + expect(svg).toBeInTheDocument(); + expect(svg).toHaveAttribute("class", testClassName); }); - describe("wordmark variant", () => { - test("renders wordmark logo correctly", () => { - const { container } = render(); - const svg = container.querySelector("svg"); + test("contains expected svg elements", () => { + const { container } = render(); + const svg = container.querySelector("svg"); - expect(svg).toBeInTheDocument(); - }); - - test("renders wordmark logo with className correctly", () => { - const testClassName = "test-class"; - const { container } = render(); - const svg = container.querySelector("svg"); - - expect(svg).toBeInTheDocument(); - expect(svg).toHaveAttribute("class", testClassName); - }); - - test("contains expected svg elements", () => { - const { container } = render(); - const svg = container.querySelector("svg"); - - expect(svg?.querySelectorAll("path").length).toBeGreaterThan(0); - expect(svg?.querySelector("line")).toBeInTheDocument(); - expect(svg?.querySelectorAll("mask").length).toBe(2); - expect(svg?.querySelectorAll("filter").length).toBe(3); - expect(svg?.querySelectorAll("linearGradient").length).toBe(6); - }); + expect(svg?.querySelectorAll("path").length).toBeGreaterThan(0); + expect(svg?.querySelector("line")).toBeInTheDocument(); + expect(svg?.querySelectorAll("mask").length).toBe(2); + expect(svg?.querySelectorAll("filter").length).toBe(3); + expect(svg?.querySelectorAll("linearGradient").length).toBe(6); }); }); diff --git a/apps/web/modules/ui/components/logo/index.tsx b/apps/web/modules/ui/components/logo/index.tsx index 03d2767b78..1993736f91 100644 --- a/apps/web/modules/ui/components/logo/index.tsx +++ b/apps/web/modules/ui/components/logo/index.tsx @@ -1,208 +1,4 @@ -interface LogoProps extends React.SVGProps { - variant?: "image" | "wordmark"; -} - -export const Logo = ({ variant = "wordmark", ...props }: LogoProps) => { - if (variant === "image") return ; - - return ; -}; - -const ImageLogo = (props: React.SVGProps) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -const WordmarkLogo = (props: React.SVGProps) => { +export const Logo = (props: any) => { return ( ; - -const meta: Meta = { - title: "UI/Logo", - component: Logo, - tags: ["autodocs"], - parameters: { - layout: "centered", - controls: { sort: "alpha", exclude: [] }, - docs: { - description: { - component: - "** Logo ** renders the Formbricks brand as scalable SVG.It supports two variants('image' and 'wordmark') and is suitable for headers, navigation, and other branding areas.", - }, - }, - }, - argTypes: { - variant: { - control: "select", - options: ["image", "wordmark"], - description: "The variant of the logo to display", - table: { - category: "Appearance", - type: { summary: "string" }, - defaultValue: { summary: "wordmark" }, - }, - order: 1, - }, - className: { - control: "text", - description: "Additional CSS classes for styling", - table: { - category: "Appearance", - type: { summary: "string" }, - }, - order: 1, - }, - }, -}; - -export default meta; -type Story = StoryObj; - -const renderLogoWithOptions = (args: StoryProps) => { - const { ...logoProps } = args; - - return ; -}; - -export const Default: Story = { - render: renderLogoWithOptions, - args: { - className: "h-20", - }, -}; - -export const Image: Story = { - render: renderLogoWithOptions, - args: { - className: "h-20", - variant: "image", - }, -}; - -export const Wordmark: Story = { - render: renderLogoWithOptions, - args: { - className: "h-20", - variant: "wordmark", - }, -}; diff --git a/apps/web/scripts/docker/next-start.sh b/apps/web/scripts/docker/next-start.sh index 9e40e7a260..03af31ae5c 100755 --- a/apps/web/scripts/docker/next-start.sh +++ b/apps/web/scripts/docker/next-start.sh @@ -55,7 +55,7 @@ else fi echo "🗃️ Running database migrations..." -run_with_timeout 600 "database migration" sh -c '(cd packages/database && npm run db:migrate:deploy)' +run_with_timeout 300 "database migration" sh -c '(cd packages/database && npm run db:migrate:deploy)' echo "🗃️ Running SAML database setup..." run_with_timeout 60 "SAML database setup" sh -c '(cd packages/database && npm run db:create-saml-database:deploy)'