mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 02:55:04 -05:00
feat: nav cleanup pt. 2 (#6515)
Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
committed by
GitHub
parent
d46644fe0d
commit
0188aad97b
-14
@@ -1,14 +0,0 @@
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
|
||||
export const ActionTableHeading = async () => {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-6 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">{t("common.edit")}</span>
|
||||
<div className="col-span-4 pl-6">{t("environments.actions.user_actions")}</div>
|
||||
<div className="col-span-2 text-center">{t("common.created")}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { CreateNewActionTab } from "@/modules/survey/editor/components/create-new-action-tab";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/modules/ui/components/dialog";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { MousePointerClickIcon, PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
|
||||
interface AddActionModalProps {
|
||||
environmentId: string;
|
||||
actionClasses: TActionClass[];
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const AddActionModal = ({ environmentId, actionClasses, isReadOnly }: AddActionModalProps) => {
|
||||
const { t } = useTranslate();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [newActionClasses, setNewActionClasses] = useState<TActionClass[]>(actionClasses);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" onClick={() => setOpen(true)}>
|
||||
{t("common.add_action")}
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent disableCloseOnOutsideClick>
|
||||
<DialogHeader>
|
||||
<MousePointerClickIcon />
|
||||
<DialogTitle>{t("environments.actions.track_new_user_action")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("environments.actions.track_user_action_to_display_surveys_or_create_user_segment")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<CreateNewActionTab
|
||||
actionClasses={newActionClasses}
|
||||
environmentId={environmentId}
|
||||
isReadOnly={isReadOnly}
|
||||
setActionClasses={setNewActionClasses}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import Loading from "./loading";
|
||||
|
||||
// Mock child components
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="page-content-wrapper">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: ({ pageTitle }: { pageTitle: string }) => <div data-testid="page-header">{pageTitle}</div>,
|
||||
}));
|
||||
|
||||
describe("Loading", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("renders loading state correctly", () => {
|
||||
render(<Loading />);
|
||||
|
||||
// Check if mocked components are rendered
|
||||
expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("page-header")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("page-header")).toHaveTextContent("common.actions");
|
||||
|
||||
// Check for translated table headers
|
||||
expect(screen.getByText("environments.actions.user_actions")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.created")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.edit")).toBeInTheDocument(); // Screen reader text
|
||||
|
||||
// Check for skeleton elements (presence of animate-pulse class)
|
||||
const skeletonElements = document.querySelectorAll(".animate-pulse");
|
||||
expect(skeletonElements.length).toBeGreaterThan(0); // Ensure some skeleton elements are rendered
|
||||
|
||||
// Check for the presence of multiple skeleton rows (3 rows * 4 pulse elements per row = 12)
|
||||
const pulseDivs = screen.getAllByText((_, element) => {
|
||||
return element?.tagName.toLowerCase() === "div" && element.classList.contains("animate-pulse");
|
||||
});
|
||||
expect(pulseDivs.length).toBe(3 * 4); // 3 rows, 4 pulsing divs per row (icon, name, desc, created)
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
|
||||
const Loading = () => {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.actions")} />
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-6 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">{t("common.edit")}</span>
|
||||
<div className="col-span-4 pl-6">{t("environments.actions.user_actions")}</div>
|
||||
<div className="col-span-2 text-center">{t("common.created")}</div>
|
||||
</div>
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="m-2 grid h-16 grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-6 w-6 flex-shrink-0 animate-pulse rounded-full bg-slate-200 text-slate-500" />
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">
|
||||
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-400">
|
||||
<div className="h-2 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto flex justify-center whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="h-4 w-28 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Loading;
|
||||
@@ -1,161 +0,0 @@
|
||||
import { getActionClasses } from "@/lib/actionClass/service";
|
||||
import { getEnvironments } from "@/lib/environment/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
// Import the component after mocks
|
||||
import Page from "./page";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/actionClass/service", () => ({
|
||||
getActionClasses: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/environment/service", () => ({
|
||||
getEnvironments: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/utils/locale", () => ({
|
||||
findMatchingLocale: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: async () => (key: string) => key,
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/actions/components/ActionClassesTable", () => ({
|
||||
ActionClassesTable: ({ children }) => <div>ActionClassesTable Mock{children}</div>,
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/actions/components/ActionRowData", () => ({
|
||||
ActionClassDataRow: ({ actionClass }) => <div>ActionClassDataRow Mock: {actionClass.name}</div>,
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading", () => ({
|
||||
ActionTableHeading: () => <div>ActionTableHeading Mock</div>,
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/actions/components/AddActionModal", () => ({
|
||||
AddActionModal: () => <div>AddActionModal Mock</div>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-content-wrapper", () => ({
|
||||
PageContentWrapper: ({ children }) => <div>PageContentWrapper Mock{children}</div>,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/page-header", () => ({
|
||||
PageHeader: ({ pageTitle, cta }) => (
|
||||
<div>
|
||||
PageHeader Mock: {pageTitle} {cta && <div>CTA Mock</div>}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock data
|
||||
const mockEnvironmentId = "test-env-id";
|
||||
const mockProjectId = "test-project-id";
|
||||
const mockEnvironment = {
|
||||
id: mockEnvironmentId,
|
||||
name: "Test Environment",
|
||||
type: "development",
|
||||
} as unknown as TEnvironment;
|
||||
const mockOtherEnvironment = {
|
||||
id: "other-env-id",
|
||||
name: "Other Environment",
|
||||
type: "production",
|
||||
} as unknown as TEnvironment;
|
||||
const mockProject = { id: mockProjectId, name: "Test Project" } as unknown as TProject;
|
||||
const mockActionClasses = [
|
||||
{ id: "action1", name: "Action 1", type: "code", environmentId: mockEnvironmentId } as TActionClass,
|
||||
{ id: "action2", name: "Action 2", type: "noCode", environmentId: mockEnvironmentId } as TActionClass,
|
||||
];
|
||||
const mockOtherEnvActionClasses = [
|
||||
{ id: "action3", name: "Action 3", type: "code", environmentId: mockOtherEnvironment.id } as TActionClass,
|
||||
];
|
||||
const mockLocale = "en-US";
|
||||
|
||||
const mockParams = { environmentId: mockEnvironmentId };
|
||||
const mockProps = { params: mockParams };
|
||||
|
||||
describe("Actions Page", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getActionClasses)
|
||||
.mockResolvedValueOnce(mockActionClasses) // First call for current env
|
||||
.mockResolvedValueOnce(mockOtherEnvActionClasses); // Second call for other env
|
||||
vi.mocked(getEnvironments).mockResolvedValue([mockEnvironment, mockOtherEnvironment]);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("renders the page correctly with actions", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
isReadOnly: false,
|
||||
project: mockProject,
|
||||
isBilling: false,
|
||||
environment: mockEnvironment,
|
||||
} as TEnvironmentAuth);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("PageHeader Mock: common.actions")).toBeInTheDocument();
|
||||
expect(screen.getByText("CTA Mock")).toBeInTheDocument(); // AddActionModal rendered via CTA
|
||||
expect(screen.getByText("ActionClassesTable Mock")).toBeInTheDocument();
|
||||
expect(screen.getByText("ActionTableHeading Mock")).toBeInTheDocument();
|
||||
expect(screen.getByText("ActionClassDataRow Mock: Action 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("ActionClassDataRow Mock: Action 2")).toBeInTheDocument();
|
||||
expect(vi.mocked(redirect)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("redirects if isBilling is true", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
isReadOnly: false,
|
||||
project: mockProject,
|
||||
isBilling: true,
|
||||
environment: mockEnvironment,
|
||||
} as TEnvironmentAuth);
|
||||
|
||||
await Page(mockProps);
|
||||
|
||||
expect(vi.mocked(redirect)).toHaveBeenCalledWith(`/environments/${mockEnvironmentId}/settings/billing`);
|
||||
});
|
||||
|
||||
test("does not render AddActionModal CTA if isReadOnly is true", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
isReadOnly: true,
|
||||
project: mockProject,
|
||||
isBilling: false,
|
||||
environment: mockEnvironment,
|
||||
} as TEnvironmentAuth);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("PageHeader Mock: common.actions")).toBeInTheDocument();
|
||||
expect(screen.queryByText("CTA Mock")).not.toBeInTheDocument(); // CTA should not be present
|
||||
expect(screen.getByText("ActionClassesTable Mock")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders AddActionModal CTA if isReadOnly is false", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
isReadOnly: false,
|
||||
project: mockProject,
|
||||
isBilling: false,
|
||||
environment: mockEnvironment,
|
||||
} as TEnvironmentAuth);
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("PageHeader Mock: common.actions")).toBeInTheDocument();
|
||||
expect(screen.getByText("CTA Mock")).toBeInTheDocument(); // CTA should be present
|
||||
expect(screen.getByText("ActionClassesTable Mock")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import { ActionClassesTable } from "@/app/(app)/environments/[environmentId]/actions/components/ActionClassesTable";
|
||||
import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData";
|
||||
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
|
||||
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
|
||||
import { getActionClasses } from "@/lib/actionClass/service";
|
||||
import { getEnvironments } from "@/lib/environment/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Actions",
|
||||
};
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { isReadOnly, project, isBilling, environment } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const t = await getTranslate();
|
||||
|
||||
const [actionClasses] = await Promise.all([getActionClasses(params.environmentId)]);
|
||||
|
||||
const locale = await findMatchingLocale();
|
||||
const environments = await getEnvironments(project.id);
|
||||
|
||||
const otherEnvironment = environments.filter((env) => env.id !== params.environmentId)[0];
|
||||
|
||||
const otherEnvActionClasses = await getActionClasses(otherEnvironment.id);
|
||||
|
||||
if (isBilling) {
|
||||
return redirect(`/environments/${params.environmentId}/settings/billing`);
|
||||
}
|
||||
|
||||
const renderAddActionButton = () => (
|
||||
<AddActionModal
|
||||
environmentId={params.environmentId}
|
||||
actionClasses={actionClasses}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.actions")} cta={!isReadOnly ? renderAddActionButton() : undefined} />
|
||||
<ActionClassesTable
|
||||
environment={environment}
|
||||
otherEnvironment={otherEnvironment}
|
||||
otherEnvActionClasses={otherEnvActionClasses}
|
||||
environmentId={params.environmentId}
|
||||
actionClasses={actionClasses}
|
||||
isReadOnly={isReadOnly}>
|
||||
<ActionTableHeading />
|
||||
{actionClasses.map((actionClass) => (
|
||||
<ActionClassDataRow key={actionClass.id} actionClass={actionClass} locale={locale} />
|
||||
))}
|
||||
</ActionClassesTable>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Code2Icon, MousePointerClickIcon } from "lucide-react";
|
||||
|
||||
export const ACTION_TYPE_ICON_LOOKUP = {
|
||||
code: <Code2Icon className="h-4 w-4" />,
|
||||
noCode: <MousePointerClickIcon className="h-4 w-4" />,
|
||||
};
|
||||
@@ -142,7 +142,6 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
isOwnerOrManager={isOwnerOrManager}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
membershipRole={membershipRole}
|
||||
projectPermission={projectPermission}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||
import { getLatestStableFbReleaseAction } from "@/modules/projects/settings/(setup)/app-connection/actions";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
@@ -7,7 +8,6 @@ import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { getLatestStableFbReleaseAction } from "../actions/actions";
|
||||
import { MainNavigation } from "./MainNavigation";
|
||||
|
||||
// Mock constants that this test needs
|
||||
@@ -32,7 +32,7 @@ vi.mock("next-auth/react", () => ({
|
||||
vi.mock("@/modules/auth/hooks/use-sign-out", () => ({
|
||||
useSignOut: vi.fn(() => ({ signOut: vi.fn() })),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/actions/actions", () => ({
|
||||
vi.mock("@/modules/projects/settings/(setup)/app-connection/actions", () => ({
|
||||
getLatestStableFbReleaseAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/app/lib/formbricks", () => ({
|
||||
@@ -197,14 +197,6 @@ describe("MainNavigation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("renders correct active navigation link", () => {
|
||||
vi.mocked(usePathname).mockReturnValue("/environments/env1/actions");
|
||||
render(<MainNavigation {...defaultProps} />);
|
||||
const actionsLink = screen.getByRole("link", { name: /common.actions/ });
|
||||
// Check if the parent li has the active class styling
|
||||
expect(actionsLink.closest("li")).toHaveClass("border-brand-dark");
|
||||
});
|
||||
|
||||
test("renders user dropdown and handles logout", async () => {
|
||||
const mockSignOut = vi.fn().mockResolvedValue({ url: "/auth/login" });
|
||||
vi.mocked(useSignOut).mockReturnValue({ signOut: mockSignOut });
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { getLatestStableFbReleaseAction } from "@/app/(app)/environments/[environmentId]/actions/actions";
|
||||
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
||||
import { isNewerVersion } from "@/app/(app)/environments/[environmentId]/lib/utils";
|
||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||
import { getLatestStableFbReleaseAction } from "@/modules/projects/settings/(setup)/app-connection/actions";
|
||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
@@ -18,12 +18,10 @@ import {
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
BlocksIcon,
|
||||
ChevronRightIcon,
|
||||
Cog,
|
||||
LogOutIcon,
|
||||
MessageCircle,
|
||||
MousePointerClick,
|
||||
PanelLeftCloseIcon,
|
||||
PanelLeftOpenIcon,
|
||||
RocketIcon,
|
||||
@@ -112,18 +110,6 @@ export const MainNavigation = ({
|
||||
icon: UserIcon,
|
||||
isActive: pathname?.includes("/contacts") || pathname?.includes("/segments"),
|
||||
},
|
||||
{
|
||||
name: t("common.actions"),
|
||||
href: `/environments/${environment.id}/actions`,
|
||||
icon: MousePointerClick,
|
||||
isActive: pathname?.includes("/actions"),
|
||||
},
|
||||
{
|
||||
name: t("common.integrations"),
|
||||
href: `/environments/${environment.id}/integrations`,
|
||||
icon: BlocksIcon,
|
||||
isActive: pathname?.includes("/integrations"),
|
||||
},
|
||||
{
|
||||
name: t("common.configuration"),
|
||||
href: `/environments/${environment.id}/project/general`,
|
||||
|
||||
@@ -3,13 +3,6 @@
|
||||
import { ProjectAndOrgSwitch } from "@/app/(app)/environments/[environmentId]/components/project-and-org-switch";
|
||||
import { useEnvironment } from "@/app/(app)/environments/[environmentId]/context/environment-context";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { BugIcon, CircleUserIcon, PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
|
||||
@@ -26,7 +19,6 @@ interface TopControlBarProps {
|
||||
isOwnerOrManager: boolean;
|
||||
isAccessControlAllowed: boolean;
|
||||
membershipRole?: TOrganizationRole;
|
||||
projectPermission: TTeamPermission | null;
|
||||
}
|
||||
|
||||
export const TopControlBar = ({
|
||||
@@ -42,70 +34,29 @@ export const TopControlBar = ({
|
||||
isOwnerOrManager,
|
||||
isAccessControlAllowed,
|
||||
membershipRole,
|
||||
projectPermission,
|
||||
}: TopControlBarProps) => {
|
||||
const { t } = useTranslate();
|
||||
|
||||
const { isMember, isBilling } = getAccessFlags(membershipRole);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
const { isMember } = getAccessFlags(membershipRole);
|
||||
const { environment } = useEnvironment();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-14 w-full items-center justify-between bg-slate-50 px-6"
|
||||
data-testid="fb__global-top-control-bar">
|
||||
<div className="flex items-center">
|
||||
<ProjectAndOrgSwitch
|
||||
currentEnvironmentId={environment.id}
|
||||
environments={environments}
|
||||
currentOrganizationId={currentOrganizationId}
|
||||
organizations={organizations}
|
||||
currentProjectId={currentProjectId}
|
||||
projects={projects}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
organizationProjectsLimit={organizationProjectsLimit}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isLicenseActive={isLicenseActive}
|
||||
isOwnerOrManager={isOwnerOrManager}
|
||||
isMember={isMember}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
/>
|
||||
</div>
|
||||
<div className="z-50 flex items-center space-x-2">
|
||||
<TooltipRenderer tooltipContent={t("common.share_feedback")}>
|
||||
<Button variant="ghost" size="icon" className="h-fit w-fit bg-slate-50 p-1" asChild>
|
||||
<Link
|
||||
href="https://github.com/formbricks/formbricks/issues"
|
||||
target="_blank"
|
||||
aria-label={t("common.share_feedback")}
|
||||
rel="noopener noreferrer">
|
||||
<BugIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
|
||||
<TooltipRenderer tooltipContent={t("common.account")}>
|
||||
<Button variant="ghost" size="icon" className="h-fit w-fit bg-slate-50 p-1" asChild>
|
||||
<Link href={`/environments/${environment.id}/settings/profile`} aria-label={t("common.account")}>
|
||||
<CircleUserIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
{isBilling || isReadOnly ? (
|
||||
<></>
|
||||
) : (
|
||||
<TooltipRenderer tooltipContent={t("common.new_survey")}>
|
||||
<Button variant="secondary" size="icon" className="h-fit w-fit p-1" asChild>
|
||||
<Link
|
||||
href={`/environments/${environment.id}/surveys/templates`}
|
||||
aria-label={t("common.new_survey")}>
|
||||
<PlusIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
</div>
|
||||
<ProjectAndOrgSwitch
|
||||
currentEnvironmentId={environment.id}
|
||||
environments={environments}
|
||||
currentOrganizationId={currentOrganizationId}
|
||||
organizations={organizations}
|
||||
currentProjectId={currentProjectId}
|
||||
projects={projects}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
organizationProjectsLimit={organizationProjectsLimit}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isLicenseActive={isLicenseActive}
|
||||
isOwnerOrManager={isOwnerOrManager}
|
||||
isMember={isMember}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+20
-40
@@ -143,10 +143,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
|
||||
test("renders environment breadcrumb with production environment", () => {
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockProductionEnvironment.id}
|
||||
/>
|
||||
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument();
|
||||
@@ -159,7 +156,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockDevelopmentEnvironment.id}
|
||||
currentEnvironment={mockDevelopmentEnvironment}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -172,7 +169,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockDevelopmentEnvironment.id}
|
||||
currentEnvironment={mockDevelopmentEnvironment}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -182,10 +179,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
|
||||
test("does not highlight breadcrumb item for production environment", () => {
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockProductionEnvironment.id}
|
||||
/>
|
||||
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
|
||||
);
|
||||
|
||||
const breadcrumbItem = screen.getByTestId("breadcrumb-item");
|
||||
@@ -195,10 +189,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
test("shows chevron down icon when dropdown is open", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockProductionEnvironment.id}
|
||||
/>
|
||||
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
|
||||
);
|
||||
|
||||
const dropdownMenu = screen.getByTestId("dropdown-menu");
|
||||
@@ -212,10 +203,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
test("renders dropdown content with environment options", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockProductionEnvironment.id}
|
||||
/>
|
||||
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
|
||||
);
|
||||
|
||||
const dropdownMenu = screen.getByTestId("dropdown-menu");
|
||||
@@ -229,10 +217,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
test("renders all environment options in dropdown", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockProductionEnvironment.id}
|
||||
/>
|
||||
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
|
||||
);
|
||||
|
||||
const dropdownMenu = screen.getByTestId("dropdown-menu");
|
||||
@@ -255,10 +240,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
test("handles environment change when clicking dropdown option", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockProductionEnvironment.id}
|
||||
/>
|
||||
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
|
||||
);
|
||||
|
||||
const dropdownMenu = screen.getByTestId("dropdown-menu");
|
||||
@@ -275,10 +257,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
|
||||
test("capitalizes environment type in display", () => {
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockProductionEnvironment.id}
|
||||
/>
|
||||
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
|
||||
);
|
||||
|
||||
const environmentSpans = screen.getAllByText("production");
|
||||
@@ -290,7 +269,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockDevelopmentEnvironment.id}
|
||||
currentEnvironment={mockDevelopmentEnvironment}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -301,10 +280,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
|
||||
test("renders without tooltip for production environment", () => {
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockProductionEnvironment.id}
|
||||
/>
|
||||
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("circle-help-icon")).not.toBeInTheDocument();
|
||||
@@ -314,10 +290,7 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
test("sets breadcrumb item as active when dropdown is open", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={mockEnvironments}
|
||||
currentEnvironmentId={mockProductionEnvironment.id}
|
||||
/>
|
||||
<EnvironmentBreadcrumb environments={mockEnvironments} currentEnvironment={mockProductionEnvironment} />
|
||||
);
|
||||
|
||||
// Initially not active
|
||||
@@ -339,11 +312,18 @@ describe("EnvironmentBreadcrumb", () => {
|
||||
render(
|
||||
<EnvironmentBreadcrumb
|
||||
environments={singleEnvironment}
|
||||
currentEnvironmentId={mockProductionEnvironment.id}
|
||||
currentEnvironment={mockProductionEnvironment}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("production")).toHaveLength(2); // trigger + dropdown option
|
||||
});
|
||||
|
||||
test("handles empty environments array gracefully", () => {
|
||||
render(<EnvironmentBreadcrumb environments={[]} currentEnvironment={mockProductionEnvironment} />);
|
||||
|
||||
expect(screen.getByTestId("breadcrumb-item")).toBeInTheDocument();
|
||||
expect(screen.getByText("production")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+4
-8
@@ -16,22 +16,18 @@ import { useState } from "react";
|
||||
|
||||
export const EnvironmentBreadcrumb = ({
|
||||
environments,
|
||||
currentEnvironmentId,
|
||||
currentEnvironment,
|
||||
}: {
|
||||
environments: { id: string; type: string }[];
|
||||
currentEnvironmentId: string;
|
||||
currentEnvironment: { id: string; type: string };
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
const [isEnvironmentDropdownOpen, setIsEnvironmentDropdownOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const currentEnvironment = environments.find((env) => env.id === currentEnvironmentId);
|
||||
|
||||
if (!currentEnvironment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleEnvironmentChange = (environmentId: string) => {
|
||||
if (environmentId === currentEnvironment.id) return;
|
||||
setIsLoading(true);
|
||||
router.push(`/environments/${environmentId}/`);
|
||||
};
|
||||
@@ -77,7 +73,7 @@ export const EnvironmentBreadcrumb = ({
|
||||
{environments.map((env) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={env.id}
|
||||
checked={env.id === currentEnvironment.id}
|
||||
checked={env.type === currentEnvironment.type}
|
||||
onClick={() => handleEnvironmentChange(env.id)}
|
||||
className="cursor-pointer">
|
||||
<div className="flex items-center gap-2 capitalize">
|
||||
|
||||
+7
-1
@@ -10,6 +10,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import {
|
||||
BuildingIcon,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
interface OrganizationBreadcrumbProps {
|
||||
currentOrganizationId: string;
|
||||
@@ -50,10 +52,14 @@ export const OrganizationBreadcrumb = ({
|
||||
const currentOrganization = organizations.find((org) => org.id === currentOrganizationId);
|
||||
|
||||
if (!currentOrganization) {
|
||||
return null;
|
||||
const errorMessage = `Organization not found for organization id: ${currentOrganizationId}`;
|
||||
logger.error(errorMessage);
|
||||
Sentry.captureException(new Error(errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
const handleOrganizationChange = (organizationId: string) => {
|
||||
if (organizationId === currentOrganizationId) return;
|
||||
setIsLoading(true);
|
||||
router.push(`/organizations/${organizationId}/`);
|
||||
};
|
||||
|
||||
+1
-12
@@ -105,7 +105,7 @@ describe("ProjectAndOrgSwitch", () => {
|
||||
id: "env-1",
|
||||
createdAt: new Date("2023-01-01"),
|
||||
updatedAt: new Date("2023-01-01"),
|
||||
type: "production",
|
||||
type: "development",
|
||||
projectId: "proj-1",
|
||||
appSetupCompleted: true,
|
||||
};
|
||||
@@ -160,7 +160,6 @@ describe("ProjectAndOrgSwitch", () => {
|
||||
|
||||
expect(screen.getByTestId("organization-breadcrumb")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("project-breadcrumb")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("environment-breadcrumb")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -234,17 +233,7 @@ describe("ProjectAndOrgSwitch", () => {
|
||||
render(<ProjectAndOrgSwitch {...defaultProps} />);
|
||||
|
||||
const envBreadcrumb = screen.getByTestId("environment-breadcrumb");
|
||||
expect(envBreadcrumb).toHaveTextContent("Environment: production");
|
||||
expect(envBreadcrumb).toHaveTextContent("Environments Count: 2");
|
||||
expect(envBreadcrumb).toHaveTextContent("Environment ID: env-1");
|
||||
});
|
||||
|
||||
test("handles development environment", () => {
|
||||
render(<ProjectAndOrgSwitch {...defaultProps} currentEnvironmentId="env-2" />);
|
||||
|
||||
const envBreadcrumb = screen.getByTestId("environment-breadcrumb");
|
||||
expect(envBreadcrumb).toHaveTextContent("Environment: development");
|
||||
expect(envBreadcrumb).toHaveTextContent("Environment ID: env-2");
|
||||
});
|
||||
|
||||
test("handles single environment", () => {
|
||||
|
||||
+5
-2
@@ -42,6 +42,8 @@ export const ProjectAndOrgSwitch = ({
|
||||
() => organizations.toSorted((a, b) => a.name.localeCompare(b.name)),
|
||||
[organizations]
|
||||
);
|
||||
const currentEnvironment = environments.find((env) => env.id === currentEnvironmentId);
|
||||
const showEnvironmentBreadcrumb = currentEnvironment?.type === "development";
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
@@ -66,10 +68,11 @@ export const ProjectAndOrgSwitch = ({
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isLicenseActive={isLicenseActive}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isEnvironmentBreadcrumbVisible={showEnvironmentBreadcrumb}
|
||||
/>
|
||||
)}
|
||||
{currentEnvironmentId && (
|
||||
<EnvironmentBreadcrumb environments={environments} currentEnvironmentId={currentEnvironmentId} />
|
||||
{showEnvironmentBreadcrumb && (
|
||||
<EnvironmentBreadcrumb environments={environments} currentEnvironment={currentEnvironment} />
|
||||
)}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
+16
-1
@@ -10,6 +10,7 @@ import { ProjectBreadcrumb } from "./project-breadcrumb";
|
||||
// Mock the dependencies
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: vi.fn(),
|
||||
usePathname: vi.fn(() => "/environments/env-123/project/general"),
|
||||
}));
|
||||
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
@@ -87,6 +88,7 @@ vi.mock("@/modules/ui/components/dropdown-menu", () => ({
|
||||
</button>
|
||||
),
|
||||
DropdownMenuGroup: ({ children }: any) => <div data-testid="dropdown-group">{children}</div>,
|
||||
DropdownMenuSeparator: () => <div data-testid="dropdown-separator" />,
|
||||
}));
|
||||
|
||||
// Mock Lucide React icons
|
||||
@@ -122,6 +124,16 @@ vi.mock("lucide-react", () => ({
|
||||
<title>Loader2 Icon</title>
|
||||
</svg>
|
||||
),
|
||||
CogIcon: ({ className }: any) => (
|
||||
<svg data-testid="cog-icon" className={className}>
|
||||
<title>Cog Icon</title>
|
||||
</svg>
|
||||
),
|
||||
SettingsIcon: ({ className }: any) => (
|
||||
<svg data-testid="settings-icon" className={className}>
|
||||
<title>Settings Icon</title>
|
||||
</svg>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("ProjectBreadcrumb", () => {
|
||||
@@ -177,6 +189,7 @@ describe("ProjectBreadcrumb", () => {
|
||||
isLicenseActive: false,
|
||||
currentEnvironmentId: "env-123",
|
||||
isAccessControlAllowed: true,
|
||||
isEnvironmentBreadcrumbVisible: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -229,7 +242,7 @@ describe("ProjectBreadcrumb", () => {
|
||||
|
||||
expect(screen.getByTestId("dropdown-content")).toBeInTheDocument();
|
||||
expect(screen.getByText("common.choose_project")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dropdown-group")).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId("dropdown-group")).toHaveLength(2); // Projects group and settings group
|
||||
});
|
||||
|
||||
test("renders all project options in dropdown", async () => {
|
||||
@@ -336,6 +349,7 @@ describe("ProjectBreadcrumb", () => {
|
||||
projects: [mockProject1, mockProject2, { ...mockProject1, id: "proj-3", name: "Project 3" }],
|
||||
organizationProjectsLimit: 3,
|
||||
isFormbricksCloud: true,
|
||||
isEnvironmentBreadcrumbVisible: true,
|
||||
currentOrganization: {
|
||||
...mockOrganization,
|
||||
billing: { ...mockOrganization.billing, plan: "startup" } as unknown as TOrganizationBilling,
|
||||
@@ -361,6 +375,7 @@ describe("ProjectBreadcrumb", () => {
|
||||
organizationProjectsLimit: 3,
|
||||
isFormbricksCloud: false,
|
||||
isLicenseActive: true,
|
||||
isEnvironmentBreadcrumbVisible: true,
|
||||
};
|
||||
render(<ProjectBreadcrumb {...props} />);
|
||||
|
||||
|
||||
@@ -8,13 +8,16 @@ import {
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
import { ModalButton } from "@/modules/ui/components/upgrade-prompt";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { ChevronDownIcon, ChevronRightIcon, FolderOpenIcon, Loader2, PlusIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ChevronDownIcon, ChevronRightIcon, CogIcon, FolderOpenIcon, Loader2, PlusIcon } from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
interface ProjectBreadcrumbProps {
|
||||
currentProjectId: string;
|
||||
@@ -26,6 +29,7 @@ interface ProjectBreadcrumbProps {
|
||||
currentOrganizationId: string;
|
||||
currentEnvironmentId: string;
|
||||
isAccessControlAllowed: boolean;
|
||||
isEnvironmentBreadcrumbVisible: boolean;
|
||||
}
|
||||
|
||||
export const ProjectBreadcrumb = ({
|
||||
@@ -38,6 +42,7 @@ export const ProjectBreadcrumb = ({
|
||||
currentOrganizationId,
|
||||
currentEnvironmentId,
|
||||
isAccessControlAllowed,
|
||||
isEnvironmentBreadcrumbVisible,
|
||||
}: ProjectBreadcrumbProps) => {
|
||||
const { t } = useTranslate();
|
||||
const [isProjectDropdownOpen, setIsProjectDropdownOpen] = useState(false);
|
||||
@@ -45,13 +50,57 @@ export const ProjectBreadcrumb = ({
|
||||
const [openLimitModal, setOpenLimitModal] = useState(false);
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
const projectSettings = [
|
||||
{
|
||||
id: "general",
|
||||
label: t("common.general"),
|
||||
href: `/environments/${currentEnvironmentId}/project/general`,
|
||||
},
|
||||
{
|
||||
id: "look",
|
||||
label: t("common.look_and_feel"),
|
||||
href: `/environments/${currentEnvironmentId}/project/look`,
|
||||
},
|
||||
{
|
||||
id: "app-connection",
|
||||
label: t("common.website_and_app_connection"),
|
||||
href: `/environments/${currentEnvironmentId}/project/app-connection`,
|
||||
},
|
||||
{
|
||||
id: "integrations",
|
||||
label: t("common.integrations"),
|
||||
href: `/environments/${currentEnvironmentId}/project/integrations`,
|
||||
},
|
||||
{
|
||||
id: "teams",
|
||||
label: t("common.team_access"),
|
||||
href: `/environments/${currentEnvironmentId}/project/teams`,
|
||||
},
|
||||
{
|
||||
id: "languages",
|
||||
label: t("common.survey_languages"),
|
||||
href: `/environments/${currentEnvironmentId}/project/languages`,
|
||||
},
|
||||
{
|
||||
id: "tags",
|
||||
label: t("common.tags"),
|
||||
href: `/environments/${currentEnvironmentId}/project/tags`,
|
||||
},
|
||||
];
|
||||
|
||||
const currentProject = projects.find((project) => project.id === currentProjectId);
|
||||
|
||||
if (!currentProject) {
|
||||
return null;
|
||||
const errorMessage = `Project not found for project id: ${currentProjectId}`;
|
||||
logger.error(errorMessage);
|
||||
Sentry.captureException(new Error(errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
const handleProjectChange = (projectId: string) => {
|
||||
if (projectId === currentProjectId) return;
|
||||
setIsLoading(true);
|
||||
router.push(`/projects/${projectId}/`);
|
||||
};
|
||||
@@ -105,14 +154,14 @@ export const ProjectBreadcrumb = ({
|
||||
{isProjectDropdownOpen ? (
|
||||
<ChevronDownIcon className="h-3 w-3" strokeWidth={1.5} />
|
||||
) : (
|
||||
<ChevronRightIcon className="h-3 w-3" strokeWidth={1.5} />
|
||||
isEnvironmentBreadcrumbVisible && <ChevronRightIcon className="h-3 w-3" strokeWidth={1.5} />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="start" className="mt-2">
|
||||
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
||||
<FolderOpenIcon className="mr-2 inline h-4 w-4" />
|
||||
<FolderOpenIcon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
||||
{t("common.choose_project")}
|
||||
</div>
|
||||
<DropdownMenuGroup>
|
||||
@@ -133,9 +182,25 @@ export const ProjectBreadcrumb = ({
|
||||
onClick={handleAddProject}
|
||||
className="w-full cursor-pointer justify-between">
|
||||
<span>{t("common.add_new_project")}</span>
|
||||
<PlusIcon className="ml-2 h-4 w-4" />
|
||||
<PlusIcon className="ml-2 h-4 w-4" strokeWidth={1.5} />
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
||||
<CogIcon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
||||
{t("common.project_configuration")}
|
||||
</div>
|
||||
{projectSettings.map((setting) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={setting.id}
|
||||
checked={pathname.includes(setting.id)}
|
||||
onClick={() => router.push(setting.href)}
|
||||
className="cursor-pointer">
|
||||
{setting.label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{/* Modals */}
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -16,11 +16,11 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { AddIntegrationModal } from "./AddIntegrationModal";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown",
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/BaseSelectDropdown",
|
||||
() => ({
|
||||
BaseSelectDropdown: ({ control, airtableArray, fetchTable, defaultValue, setValue }) => (
|
||||
<div>
|
||||
@@ -44,7 +44,7 @@ vi.mock(
|
||||
),
|
||||
})
|
||||
);
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable", () => ({
|
||||
fetchTables: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
+5
-15
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown";
|
||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/BaseSelectDropdown";
|
||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable";
|
||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
TIntegrationAirtableTables,
|
||||
} from "@formbricks/types/integration/airtable";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { IntegrationModalInputs } from "../lib/types";
|
||||
|
||||
type EditModeProps =
|
||||
| { isEditMode: false; defaultData?: never }
|
||||
@@ -55,17 +56,6 @@ type AddIntegrationModalProps = {
|
||||
airtableIntegration: TIntegrationAirtable;
|
||||
} & EditModeProps;
|
||||
|
||||
export type IntegrationModalInputs = {
|
||||
base: string;
|
||||
table: string;
|
||||
survey: string;
|
||||
questions: string[];
|
||||
includeVariables: boolean;
|
||||
includeHiddenFields: boolean;
|
||||
includeMetadata: boolean;
|
||||
includeCreatedAt: boolean;
|
||||
};
|
||||
|
||||
const NoBaseFoundError = () => {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
@@ -239,7 +229,7 @@ export const AddIntegrationModal = ({
|
||||
|
||||
if (isEditMode) {
|
||||
// update action
|
||||
airtableIntegrationData.config!.data[defaultData.index] = integrationData;
|
||||
airtableIntegrationData.config.data[defaultData.index] = integrationData;
|
||||
} else {
|
||||
// create action
|
||||
airtableIntegrationData.config?.data.push(integrationData);
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -8,7 +8,7 @@ import { AirtableWrapper } from "./AirtableWrapper";
|
||||
|
||||
// Mock child components
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration",
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/ManageIntegration",
|
||||
() => ({
|
||||
ManageIntegration: ({ setIsConnected }) => (
|
||||
<div data-testid="manage-integration">
|
||||
@@ -28,7 +28,7 @@ vi.mock("@/modules/ui/components/connect-integration", () => ({
|
||||
}));
|
||||
|
||||
// Mock library function
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable", () => ({
|
||||
authorize: vi.fn(),
|
||||
}));
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/ManageIntegration";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable";
|
||||
import airtableLogo from "@/images/airtableLogo.svg";
|
||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||
import { useState } from "react";
|
||||
+1
-1
@@ -11,7 +11,7 @@ import {
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { Control, Controller, UseFormSetValue } from "react-hook-form";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { IntegrationModalInputs } from "./AddIntegrationModal";
|
||||
import { IntegrationModalInputs } from "../lib/types";
|
||||
|
||||
interface BaseSelectProps {
|
||||
control: Control<IntegrationModalInputs, any>;
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -6,11 +6,11 @@ import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationAirtable, TIntegrationAirtableConfig } from "@formbricks/types/integration/airtable";
|
||||
import { ManageIntegration } from "./ManageIntegration";
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
|
||||
deleteIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal",
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AddIntegrationModal",
|
||||
() => ({
|
||||
AddIntegrationModal: ({ open, setOpenWithStates }) =>
|
||||
open ? (
|
||||
+3
-5
@@ -1,10 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import {
|
||||
AddIntegrationModal,
|
||||
IntegrationModalInputs,
|
||||
} from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AddIntegrationModal";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -19,6 +16,7 @@ import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { IntegrationModalInputs } from "../lib/types";
|
||||
|
||||
interface ManageIntegrationProps {
|
||||
airtableIntegration: TIntegrationAirtable;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export type IntegrationModalInputs = {
|
||||
base: string;
|
||||
table: string;
|
||||
survey: string;
|
||||
questions: string[];
|
||||
includeVariables: boolean;
|
||||
includeHiddenFields: boolean;
|
||||
includeMetadata: boolean;
|
||||
includeCreatedAt: boolean;
|
||||
};
|
||||
+11
-8
@@ -1,4 +1,4 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
|
||||
import { getAirtableTables } from "@/lib/airtable/service";
|
||||
import { WEBAPP_URL } from "@/lib/constants";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
@@ -15,10 +15,13 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import Page from "./page";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper", () => ({
|
||||
AirtableWrapper: vi.fn(() => <div>AirtableWrapper Mock</div>),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys");
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper",
|
||||
() => ({
|
||||
AirtableWrapper: vi.fn(() => <div>AirtableWrapper Mock</div>),
|
||||
})
|
||||
);
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys");
|
||||
vi.mock("@/lib/airtable/service");
|
||||
|
||||
let mockAirtableClientId: string | undefined = "test-client-id";
|
||||
@@ -139,7 +142,7 @@ describe("Airtable Integration Page", () => {
|
||||
const AirtableWrapper = vi.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper"
|
||||
)
|
||||
).AirtableWrapper
|
||||
);
|
||||
@@ -175,7 +178,7 @@ describe("Airtable Integration Page", () => {
|
||||
const AirtableWrapper = vi.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper"
|
||||
)
|
||||
).AirtableWrapper
|
||||
);
|
||||
@@ -206,7 +209,7 @@ describe("Airtable Integration Page", () => {
|
||||
const AirtableWrapper = vi.mocked(
|
||||
(
|
||||
await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper"
|
||||
)
|
||||
).AirtableWrapper
|
||||
);
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AirtableWrapper";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
|
||||
import { getAirtableTables } from "@/lib/airtable/service";
|
||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@/lib/constants";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
@@ -42,7 +42,7 @@ const Page = async (props) => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/integrations`} />
|
||||
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/project/integrations`} />
|
||||
<PageHeader pageTitle={t("environments.integrations.airtable.airtable_integration")} />
|
||||
<div className="h-[75vh] w-full">
|
||||
<AirtableWrapper
|
||||
+6
-7
@@ -1,8 +1,7 @@
|
||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal";
|
||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/AddIntegrationModal";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
TIntegrationGoogleSheetsConfigData,
|
||||
@@ -10,13 +9,13 @@ import {
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock actions and utilities
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/actions", () => ({
|
||||
getSpreadsheetNameByIdAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/util", () => ({
|
||||
constructGoogleSheetsUrl: (id: string) => `https://docs.google.com/spreadsheets/d/${id}`,
|
||||
extractSpreadsheetIdFromUrl: (url: string) => url.split("/")[5],
|
||||
isValidGoogleSheetsUrl: (url: string) => url.startsWith("https://docs.google.com/spreadsheets/d/"),
|
||||
@@ -164,11 +163,11 @@ vi.mock("@tolgee/react", async () => {
|
||||
|
||||
// Mock dependencies
|
||||
const createOrUpdateIntegrationAction = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/integrations/actions"))
|
||||
(await import("@/app/(app)/environments/[environmentId]/project/integrations/actions"))
|
||||
.createOrUpdateIntegrationAction
|
||||
);
|
||||
const getSpreadsheetNameByIdAction = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions"))
|
||||
(await import("@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/actions"))
|
||||
.getSpreadsheetNameByIdAction
|
||||
);
|
||||
const toast = vi.mocked((await import("react-hot-toast")).default);
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { getSpreadsheetNameByIdAction } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { getSpreadsheetNameByIdAction } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/actions";
|
||||
import {
|
||||
constructGoogleSheetsUrl,
|
||||
extractSpreadsheetIdFromUrl,
|
||||
isValidGoogleSheetsUrl,
|
||||
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
||||
} from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/util";
|
||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
@@ -154,10 +154,10 @@ export const AddIntegrationModal = ({
|
||||
integrationData.includeCreatedAt = includeCreatedAt;
|
||||
if (selectedIntegration) {
|
||||
// update action
|
||||
googleSheetIntegrationData.config!.data[selectedIntegration.index] = integrationData;
|
||||
googleSheetIntegrationData.config.data[selectedIntegration.index] = integrationData;
|
||||
} else {
|
||||
// create action
|
||||
googleSheetIntegrationData.config!.data.push(integrationData);
|
||||
googleSheetIntegrationData.config.data.push(integrationData);
|
||||
}
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
|
||||
if (selectedIntegration) {
|
||||
@@ -196,7 +196,7 @@ export const AddIntegrationModal = ({
|
||||
};
|
||||
|
||||
const deleteLink = async () => {
|
||||
googleSheetIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
googleSheetIntegrationData.config.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/google";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
@@ -12,7 +12,7 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock child components and functions
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration",
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/ManageIntegration",
|
||||
() => ({
|
||||
ManageIntegration: vi.fn(({ setOpenAddIntegrationModal }) => (
|
||||
<div data-testid="manage-integration">
|
||||
@@ -31,7 +31,7 @@ vi.mock("@/modules/ui/components/connect-integration", () => ({
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal",
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/AddIntegrationModal",
|
||||
() => ({
|
||||
AddIntegrationModal: vi.fn(({ open }) =>
|
||||
open ? <div data-testid="add-integration-modal">Modal</div> : null
|
||||
@@ -39,7 +39,7 @@ vi.mock(
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/google", () => ({
|
||||
authorize: vi.fn(() => Promise.resolve("http://google.com/auth")),
|
||||
}));
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("GoogleSheetWrapper", () => {
|
||||
|
||||
expect(vi.mocked(authorize)).toHaveBeenCalledWith(mockEnvironment.id, mockWebAppUrl);
|
||||
// Need to wait for the promise returned by authorize to resolve
|
||||
await vi.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith("http://google.com/auth");
|
||||
});
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google";
|
||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/ManageIntegration";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/google";
|
||||
import googleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||
import { useState } from "react";
|
||||
@@ -34,7 +34,7 @@ export const GoogleSheetWrapper = ({
|
||||
const [isConnected, setIsConnected] = useState(
|
||||
googleSheetIntegration ? googleSheetIntegration.config?.key : false
|
||||
);
|
||||
const [isModalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const [selectedIntegration, setSelectedIntegration] = useState<
|
||||
(TIntegrationGoogleSheetsConfigData & { index: number }) | null
|
||||
>(null);
|
||||
@@ -55,14 +55,14 @@ export const GoogleSheetWrapper = ({
|
||||
environmentId={environment.id}
|
||||
surveys={surveys}
|
||||
open={isModalOpen}
|
||||
setOpen={setModalOpen}
|
||||
setOpen={setIsModalOpen}
|
||||
googleSheetIntegration={googleSheetIntegration}
|
||||
selectedIntegration={selectedIntegration}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
googleSheetIntegration={googleSheetIntegration}
|
||||
setOpenAddIntegrationModal={setModalOpen}
|
||||
setOpenAddIntegrationModal={setIsModalOpen}
|
||||
setIsConnected={setIsConnected}
|
||||
setSelectedIntegration={setSelectedIntegration}
|
||||
locale={locale}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -6,7 +6,7 @@ import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
import { ManageIntegration } from "./ManageIntegration";
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
|
||||
deleteIntegrationAction: vi.fn(),
|
||||
}));
|
||||
|
||||
+16
-19
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -108,24 +108,21 @@ export const ManageIntegration = ({
|
||||
<div className="col-span-2 hidden text-center sm:block">{t("common.questions")}</div>
|
||||
<div className="col-span-2 hidden text-center sm:block">{t("common.updated_at")}</div>
|
||||
</div>
|
||||
{integrationArray &&
|
||||
integrationArray.map((data, index) => {
|
||||
return (
|
||||
<button
|
||||
key={`${index}-${data.spreadsheetName}-${data.surveyName}`}
|
||||
className="grid h-16 w-full cursor-pointer grid-cols-8 content-center rounded-lg p-2 hover:bg-slate-100"
|
||||
onClick={() => {
|
||||
editIntegration(index);
|
||||
}}>
|
||||
<div className="col-span-2 text-center">{data.surveyName}</div>
|
||||
<div className="col-span-2 text-center">{data.spreadsheetName}</div>
|
||||
<div className="col-span-2 text-center">{data.questions}</div>
|
||||
<div className="col-span-2 text-center">
|
||||
{timeSince(data.createdAt.toString(), locale)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{integrationArray.map((data, index) => {
|
||||
return (
|
||||
<button
|
||||
key={`${index}-${data.spreadsheetName}-${data.surveyName}`}
|
||||
className="grid h-16 w-full cursor-pointer grid-cols-8 content-center rounded-lg p-2 hover:bg-slate-100"
|
||||
onClick={() => {
|
||||
editIntegration(index);
|
||||
}}>
|
||||
<div className="col-span-2 text-center">{data.surveyName}</div>
|
||||
<div className="col-span-2 text-center">{data.spreadsheetName}</div>
|
||||
<div className="col-span-2 text-center">{data.questions}</div>
|
||||
<div className="col-span-2 text-center">{timeSince(data.createdAt.toString(), locale)}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
export const extractSpreadsheetIdFromUrl = (url: string): string => {
|
||||
const regex = /\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/;
|
||||
const match = url.match(regex);
|
||||
if (match && match[1]) {
|
||||
const match = regex.exec(url);
|
||||
if (match?.[1]) {
|
||||
return match[1];
|
||||
} else {
|
||||
throw new Error("Invalid Google Sheets URL");
|
||||
+1
-1
@@ -27,7 +27,7 @@ const Loading = () => {
|
||||
<div className="grid-cols-7">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
key={`${index.toString()}-google-sheet-loading`}
|
||||
className="mt-2 grid h-16 grid-cols-12 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="text-left">
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/page";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/page";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
@@ -16,7 +16,7 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper",
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/GoogleSheetWrapper",
|
||||
() => ({
|
||||
GoogleSheetWrapper: vi.fn(
|
||||
({ isEnabled, environment, surveys, googleSheetIntegration, webAppUrl, locale }) => (
|
||||
@@ -33,7 +33,7 @@ vi.mock(
|
||||
),
|
||||
})
|
||||
);
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys", () => ({
|
||||
getSurveys: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -178,7 +178,7 @@ describe("GoogleSheetsIntegrationPage", () => {
|
||||
expect(screen.getByTestId("webAppUrl")).toHaveTextContent("test-webapp-url");
|
||||
expect(screen.getByTestId("locale")).toHaveTextContent("en-US");
|
||||
expect(screen.getByTestId("go-back")).toHaveTextContent(
|
||||
`test-webapp-url/environments/${mockProps.params.environmentId}/integrations`
|
||||
`test-webapp-url/environments/${mockProps.params.environmentId}/project/integrations`
|
||||
);
|
||||
expect(vi.mocked(redirect)).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -199,7 +199,7 @@ describe("GoogleSheetsIntegrationPage", () => {
|
||||
mockGoogleSheetClientId = undefined;
|
||||
|
||||
const { default: PageWithMissingConstants } = (await import(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/google-sheets/page"
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/page"
|
||||
)) as { default: typeof Page };
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue({
|
||||
environment: mockEnvironment,
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
|
||||
import {
|
||||
GOOGLE_SHEETS_CLIENT_ID,
|
||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||
@@ -40,7 +40,7 @@ const Page = async (props) => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/integrations`} />
|
||||
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/project/integrations`} />
|
||||
<PageHeader pageTitle={t("environments.integrations.google_sheets.google_sheets_integration")} />
|
||||
<div className="h-[75vh] w-full">
|
||||
<GoogleSheetWrapper
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/AddIntegrationModal";
|
||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/project/integrations/notion/components/AddIntegrationModal";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock actions and utilities
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
@@ -184,7 +184,7 @@ vi.mock("@tolgee/react", async () => {
|
||||
|
||||
// Mock dependencies
|
||||
const createOrUpdateIntegrationAction = vi.mocked(
|
||||
(await import("@/app/(app)/environments/[environmentId]/integrations/actions"))
|
||||
(await import("@/app/(app)/environments/[environmentId]/project/integrations/actions"))
|
||||
.createOrUpdateIntegrationAction
|
||||
);
|
||||
const toast = vi.mocked((await import("react-hot-toast")).default);
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import {
|
||||
ERRORS,
|
||||
TYPE_MAPPING,
|
||||
UNSUPPORTED_TYPES_BY_NOTION,
|
||||
} from "@/app/(app)/environments/[environmentId]/integrations/notion/constants";
|
||||
} from "@/app/(app)/environments/[environmentId]/project/integrations/notion/constants";
|
||||
import NotionLogo from "@/images/notion.png";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
@@ -221,10 +221,10 @@ export const AddIntegrationModal = ({
|
||||
|
||||
if (selectedIntegration) {
|
||||
// update action
|
||||
notionIntegrationData.config!.data[selectedIntegration.index] = integrationData;
|
||||
notionIntegrationData.config.data[selectedIntegration.index] = integrationData;
|
||||
} else {
|
||||
// create action
|
||||
notionIntegrationData.config!.data.push(integrationData);
|
||||
notionIntegrationData.config.data.push(integrationData);
|
||||
}
|
||||
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
|
||||
@@ -243,7 +243,7 @@ export const AddIntegrationModal = ({
|
||||
};
|
||||
|
||||
const deleteLink = async () => {
|
||||
notionIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
notionIntegrationData.config.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
|
||||
+4
-2
@@ -9,9 +9,11 @@ import type {
|
||||
} from "@formbricks/types/integration/notion";
|
||||
import { ManageIntegration } from "./ManageIntegration";
|
||||
|
||||
vi.mock("react-hot-toast", () => ({ success: vi.fn(), error: vi.fn() }));
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: { success: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
vi.mock("@/lib/time", () => ({ timeSince: () => "ago" }));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
|
||||
deleteIntegrationAction: vi.fn(),
|
||||
}));
|
||||
|
||||
+15
-18
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -118,23 +118,20 @@ export const ManageIntegration = ({
|
||||
</div>
|
||||
<div className="col-span-2 hidden text-center sm:block">{t("common.updated_at")}</div>
|
||||
</div>
|
||||
{integrationArray &&
|
||||
integrationArray.map((data, index) => {
|
||||
return (
|
||||
<button
|
||||
key={`${index}-${data.databaseId}`}
|
||||
className="grid h-16 w-full cursor-pointer grid-cols-6 content-center rounded-lg p-2 hover:bg-slate-100"
|
||||
onClick={() => {
|
||||
editIntegration(index);
|
||||
}}>
|
||||
<div className="col-span-2 text-center">{data.surveyName}</div>
|
||||
<div className="col-span-2 text-center">{data.databaseName}</div>
|
||||
<div className="col-span-2 text-center">
|
||||
{timeSince(data.createdAt.toString(), locale)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{integrationArray.map((data, index) => {
|
||||
return (
|
||||
<button
|
||||
key={`${index}-${data.databaseId}`}
|
||||
className="grid h-16 w-full cursor-pointer grid-cols-6 content-center rounded-lg p-2 hover:bg-slate-100"
|
||||
onClick={() => {
|
||||
editIntegration(index);
|
||||
}}>
|
||||
<div className="col-span-2 text-center">{data.surveyName}</div>
|
||||
<div className="col-span-2 text-center">{data.databaseName}</div>
|
||||
<div className="col-span-2 text-center">{timeSince(data.createdAt.toString(), locale)}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/notion/lib/notion";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/notion/lib/notion";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -61,7 +61,7 @@ vi.mock("@/modules/ui/components/connect-integration", () => ({
|
||||
}));
|
||||
|
||||
// Mock library function
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/notion/lib/notion", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/notion/lib/notion", () => ({
|
||||
authorize: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -140,7 +140,7 @@ describe("NotionWrapper", () => {
|
||||
test("calls authorize and redirects when Connect button is clicked", async () => {
|
||||
const mockAuthorize = vi.mocked(authorize);
|
||||
const redirectUrl = "https://notion.com/auth";
|
||||
mockAuthorize.mockResolvedValue(redirectUrl);
|
||||
mockAuthorize.mockImplementation(() => Promise.resolve(redirectUrl));
|
||||
|
||||
render(<NotionWrapper {...baseProps} enabled={true} notionIntegration={undefined} />); // Changed isEnabled to enabled
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/AddIntegrationModal";
|
||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration";
|
||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/project/integrations/notion/components/AddIntegrationModal";
|
||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/project/integrations/notion/components/ManageIntegration";
|
||||
import notionLogo from "@/images/notion.png";
|
||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||
import { useState } from "react";
|
||||
@@ -34,7 +34,7 @@ export const NotionWrapper = ({
|
||||
databasesArray,
|
||||
locale,
|
||||
}: NotionWrapperProps) => {
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(
|
||||
notionIntegration ? notionIntegration.config.key?.bot_id : false
|
||||
);
|
||||
@@ -58,7 +58,7 @@ export const NotionWrapper = ({
|
||||
environmentId={environment.id}
|
||||
surveys={surveys}
|
||||
open={isModalOpen}
|
||||
setOpen={setModalOpen}
|
||||
setOpen={setIsModalOpen}
|
||||
notionIntegration={notionIntegration}
|
||||
databases={databasesArray}
|
||||
selectedIntegration={selectedIntegration}
|
||||
@@ -66,7 +66,7 @@ export const NotionWrapper = ({
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
notionIntegration={notionIntegration}
|
||||
setOpenAddIntegrationModal={setModalOpen}
|
||||
setOpenAddIntegrationModal={setIsModalOpen}
|
||||
setIsConnected={setIsConnected}
|
||||
setSelectedIntegration={setSelectedIntegration}
|
||||
locale={locale}
|
||||
+1
-1
@@ -24,7 +24,7 @@ const Loading = () => {
|
||||
<div className="grid-cols-7">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
key={`${index.toString()}-notion-loading`}
|
||||
className="mt-2 grid h-16 grid-cols-12 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="text-left">
|
||||
+22
-19
@@ -1,5 +1,5 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/notion/page";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/project/integrations/notion/page";
|
||||
import { getIntegrationByType } from "@/lib/integration/service";
|
||||
import { getNotionDatabases } from "@/lib/notion/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
@@ -13,23 +13,26 @@ import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/type
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper", () => ({
|
||||
NotionWrapper: vi.fn(
|
||||
({ enabled, environment, surveys, notionIntegration, webAppUrl, databasesArray, locale }) => (
|
||||
<div>
|
||||
<span>Mocked NotionWrapper</span>
|
||||
<span data-testid="enabled">{enabled.toString()}</span>
|
||||
<span data-testid="environmentId">{environment.id}</span>
|
||||
<span data-testid="surveyCount">{surveys?.length ?? 0}</span>
|
||||
<span data-testid="integrationId">{notionIntegration?.id}</span>
|
||||
<span data-testid="webAppUrl">{webAppUrl}</span>
|
||||
<span data-testid="databaseCount">{databasesArray?.length ?? 0}</span>
|
||||
<span data-testid="locale">{locale}</span>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys", () => ({
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/notion/components/NotionWrapper",
|
||||
() => ({
|
||||
NotionWrapper: vi.fn(
|
||||
({ enabled, environment, surveys, notionIntegration, webAppUrl, databasesArray, locale }) => (
|
||||
<div>
|
||||
<span>Mocked NotionWrapper</span>
|
||||
<span data-testid="enabled">{enabled.toString()}</span>
|
||||
<span data-testid="environmentId">{environment.id}</span>
|
||||
<span data-testid="surveyCount">{surveys?.length ?? 0}</span>
|
||||
<span data-testid="integrationId">{notionIntegration?.id}</span>
|
||||
<span data-testid="webAppUrl">{webAppUrl}</span>
|
||||
<span data-testid="databaseCount">{databasesArray?.length ?? 0}</span>
|
||||
<span data-testid="locale">{locale}</span>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
})
|
||||
);
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys", () => ({
|
||||
getSurveys: vi.fn(),
|
||||
}));
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
|
||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/notion/components/NotionWrapper";
|
||||
import {
|
||||
NOTION_AUTH_URL,
|
||||
NOTION_OAUTH_CLIENT_ID,
|
||||
+3
-5
@@ -1,5 +1,5 @@
|
||||
import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/integrations/lib/webhook";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/page";
|
||||
import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/webhook";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/project/integrations/page";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
@@ -10,7 +10,7 @@ import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegration } from "@formbricks/types/integration";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/webhook", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/lib/webhook", () => ({
|
||||
getWebhookCountBySource: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -106,8 +106,6 @@ describe("Integrations Page", () => {
|
||||
|
||||
const PageComponent = await Page(mockProps);
|
||||
render(PageComponent);
|
||||
|
||||
expect(screen.getByText("common.integrations")).toBeInTheDocument(); // Page Header
|
||||
expect(screen.getByTestId("card-Javascript SDK")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.integrations.website_or_app_integration_description")
|
||||
+22
-37
@@ -1,4 +1,4 @@
|
||||
import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/integrations/lib/webhook";
|
||||
import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/webhook";
|
||||
import ActivePiecesLogo from "@/images/activepieces.webp";
|
||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||
import GoogleSheetsLogo from "@/images/googleSheetsLogo.png";
|
||||
@@ -11,14 +11,22 @@ import WebhookLogo from "@/images/webhook.png";
|
||||
import ZapierLogo from "@/images/zapier-small.png";
|
||||
import { getIntegrations } from "@/lib/integration/service";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation";
|
||||
import { Card } from "@/modules/ui/components/integration-card";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { TFnType } from "@tolgee/react";
|
||||
import Image from "next/image";
|
||||
import { redirect } from "next/navigation";
|
||||
import { TIntegrationType } from "@formbricks/types/integration";
|
||||
|
||||
const getStatusText = (count: number, t: TFnType, type: string) => {
|
||||
if (count === 1) return `1 ${type}`;
|
||||
if (count === 0) return t("common.not_connected");
|
||||
return `${count} ${type}s`;
|
||||
};
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
@@ -67,16 +75,11 @@ const Page = async (props) => {
|
||||
description: t("environments.integrations.zapier_integration_description"),
|
||||
icon: <Image src={ZapierLogo} alt="Zapier Logo" />,
|
||||
connected: zapierWebhookCount > 0,
|
||||
statusText:
|
||||
zapierWebhookCount === 1
|
||||
? "1 zap"
|
||||
: zapierWebhookCount === 0
|
||||
? t("common.not_connected")
|
||||
: `${zapierWebhookCount} zaps`,
|
||||
statusText: getStatusText(zapierWebhookCount, t, "zap"),
|
||||
disabled: isReadOnly,
|
||||
},
|
||||
{
|
||||
connectHref: `/environments/${params.environmentId}/integrations/webhooks`,
|
||||
connectHref: `/environments/${params.environmentId}/project/integrations/webhooks`,
|
||||
connectText: t("environments.integrations.manage_webhooks"),
|
||||
connectNewTab: false,
|
||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/webhooks",
|
||||
@@ -86,16 +89,11 @@ const Page = async (props) => {
|
||||
description: t("environments.integrations.webhook_integration_description"),
|
||||
icon: <Image src={WebhookLogo} alt="Webhook Logo" />,
|
||||
connected: userWebhookCount > 0,
|
||||
statusText:
|
||||
userWebhookCount === 1
|
||||
? "1 webhook"
|
||||
: userWebhookCount === 0
|
||||
? t("common.not_connected")
|
||||
: `${userWebhookCount} webhooks`,
|
||||
statusText: getStatusText(userWebhookCount, t, "webhook"),
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
connectHref: `/environments/${params.environmentId}/integrations/google-sheets`,
|
||||
connectHref: `/environments/${params.environmentId}/project/integrations/google-sheets`,
|
||||
connectText: `${isGoogleSheetsIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
||||
connectNewTab: false,
|
||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/google-sheets",
|
||||
@@ -109,7 +107,7 @@ const Page = async (props) => {
|
||||
disabled: isReadOnly,
|
||||
},
|
||||
{
|
||||
connectHref: `/environments/${params.environmentId}/integrations/airtable`,
|
||||
connectHref: `/environments/${params.environmentId}/project/integrations/airtable`,
|
||||
connectText: `${isAirtableIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
||||
connectNewTab: false,
|
||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/airtable",
|
||||
@@ -123,7 +121,7 @@ const Page = async (props) => {
|
||||
disabled: isReadOnly,
|
||||
},
|
||||
{
|
||||
connectHref: `/environments/${params.environmentId}/integrations/slack`,
|
||||
connectHref: `/environments/${params.environmentId}/project/integrations/slack`,
|
||||
connectText: `${isSlackIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
||||
connectNewTab: false,
|
||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/slack",
|
||||
@@ -147,12 +145,7 @@ const Page = async (props) => {
|
||||
description: t("environments.integrations.n8n_integration_description"),
|
||||
icon: <Image src={n8nLogo} alt="n8n Logo" />,
|
||||
connected: n8nwebhookCount > 0,
|
||||
statusText:
|
||||
n8nwebhookCount === 1
|
||||
? `1 ${t("common.integration")}`
|
||||
: n8nwebhookCount === 0
|
||||
? t("common.not_connected")
|
||||
: `${n8nwebhookCount} ${t("common.integrations")}`,
|
||||
statusText: getStatusText(n8nwebhookCount, t, t("common.integration")),
|
||||
disabled: isReadOnly,
|
||||
},
|
||||
{
|
||||
@@ -166,16 +159,11 @@ const Page = async (props) => {
|
||||
description: t("environments.integrations.make_integration_description"),
|
||||
icon: <Image src={MakeLogo} alt="Make Logo" />,
|
||||
connected: makeWebhookCount > 0,
|
||||
statusText:
|
||||
makeWebhookCount === 1
|
||||
? `1 ${t("common.integration")}`
|
||||
: makeWebhookCount === 0
|
||||
? t("common.not_connected")
|
||||
: `${makeWebhookCount} ${t("common.integrations")}`,
|
||||
statusText: getStatusText(makeWebhookCount, t, t("common.integration")),
|
||||
disabled: isReadOnly,
|
||||
},
|
||||
{
|
||||
connectHref: `/environments/${params.environmentId}/integrations/notion`,
|
||||
connectHref: `/environments/${params.environmentId}/project/integrations/notion`,
|
||||
connectText: `${isNotionIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
||||
connectNewTab: false,
|
||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/notion",
|
||||
@@ -199,12 +187,7 @@ const Page = async (props) => {
|
||||
description: t("environments.integrations.activepieces_integration_description"),
|
||||
icon: <Image src={ActivePiecesLogo} alt="ActivePieces Logo" />,
|
||||
connected: activePiecesWebhookCount > 0,
|
||||
statusText:
|
||||
activePiecesWebhookCount === 1
|
||||
? `1 ${t("common.integration")}`
|
||||
: activePiecesWebhookCount === 0
|
||||
? t("common.not_connected")
|
||||
: `${activePiecesWebhookCount} ${t("common.integrations")}`,
|
||||
statusText: getStatusText(activePiecesWebhookCount, t, t("common.integration")),
|
||||
disabled: isReadOnly,
|
||||
},
|
||||
];
|
||||
@@ -226,7 +209,9 @@ const Page = async (props) => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.integrations")} />
|
||||
<PageHeader pageTitle={t("common.project_configuration")}>
|
||||
<ProjectConfigNavigation environmentId={params.environmentId} activeId="integrations" />
|
||||
</PageHeader>
|
||||
<div className="grid grid-cols-3 place-content-stretch gap-4 lg:grid-cols-3">
|
||||
{integrationCards.map((card) => (
|
||||
<Card
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -12,7 +12,7 @@ import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/t
|
||||
import { AddChannelMappingModal } from "./AddChannelMappingModal";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
|
||||
createOrUpdateIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import SlackLogo from "@/images/slacklogo.png";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||
@@ -133,10 +133,10 @@ export const AddChannelMappingModal = ({
|
||||
};
|
||||
if (selectedIntegration) {
|
||||
// update action
|
||||
slackIntegrationData.config!.data[selectedIntegration.index] = integrationData;
|
||||
slackIntegrationData.config.data[selectedIntegration.index] = integrationData;
|
||||
} else {
|
||||
// create action
|
||||
slackIntegrationData.config!.data.push(integrationData);
|
||||
slackIntegrationData.config.data.push(integrationData);
|
||||
}
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
|
||||
if (selectedIntegration) {
|
||||
@@ -172,7 +172,7 @@ export const AddChannelMappingModal = ({
|
||||
};
|
||||
|
||||
const deleteLink = async () => {
|
||||
slackIntegrationData.config!.data.splice(selectedIntegration!.index, 1);
|
||||
slackIntegrationData.config.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -6,7 +6,7 @@ import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
||||
import { ManageIntegration } from "./ManageIntegration";
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/actions", () => ({
|
||||
deleteIntegrationAction: vi.fn(),
|
||||
}));
|
||||
vi.mock("react-hot-toast", () => ({ default: { success: vi.fn(), error: vi.fn() } }));
|
||||
+16
-19
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -124,24 +124,21 @@ export const ManageIntegration = ({
|
||||
<div className="col-span-2 hidden text-center sm:block">{t("common.questions")}</div>
|
||||
<div className="col-span-2 hidden text-center sm:block">{t("common.updated_at")}</div>
|
||||
</div>
|
||||
{integrationArray &&
|
||||
integrationArray.map((data, index) => {
|
||||
return (
|
||||
<button
|
||||
key={`${index}-${data.surveyName}-${data.channelName}`}
|
||||
className="grid h-16 w-full grid-cols-8 content-center rounded-lg p-2 text-slate-700 hover:cursor-pointer hover:bg-slate-100"
|
||||
onClick={() => {
|
||||
editIntegration(index);
|
||||
}}>
|
||||
<div className="col-span-2 text-center">{data.surveyName}</div>
|
||||
<div className="col-span-2 text-center">{data.channelName}</div>
|
||||
<div className="col-span-2 text-center">{data.questions}</div>
|
||||
<div className="col-span-2 text-center">
|
||||
{timeSince(data.createdAt.toString(), locale)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{integrationArray.map((data, index) => {
|
||||
return (
|
||||
<button
|
||||
key={`${index}-${data.surveyName}-${data.channelName}`}
|
||||
className="grid h-16 w-full grid-cols-8 content-center rounded-lg p-2 text-slate-700 hover:cursor-pointer hover:bg-slate-100"
|
||||
onClick={() => {
|
||||
editIntegration(index);
|
||||
}}>
|
||||
<div className="col-span-2 text-center">{data.surveyName}</div>
|
||||
<div className="col-span-2 text-center">{data.channelName}</div>
|
||||
<div className="col-span-2 text-center">{data.questions}</div>
|
||||
<div className="col-span-2 text-center">{timeSince(data.createdAt.toString(), locale)}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
+18
-15
@@ -1,3 +1,6 @@
|
||||
import { getSlackChannelsAction } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/actions";
|
||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/lib/slack";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -6,33 +9,33 @@ import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationSlack, TIntegrationSlackCredential } from "@formbricks/types/integration/slack";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { getSlackChannelsAction } from "../actions";
|
||||
import { authorize } from "../lib/slack";
|
||||
import { SlackWrapper } from "./SlackWrapper";
|
||||
|
||||
// Mock child components and actions
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/actions", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/slack/actions", () => ({
|
||||
getSlackChannelsAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/integrations/slack/components/AddChannelMappingModal",
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/slack/components/AddChannelMappingModal",
|
||||
() => ({
|
||||
AddChannelMappingModal: vi.fn(({ open }) => (open ? <div data-testid="add-modal">Add Modal</div> : null)),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration", () => ({
|
||||
ManageIntegration: vi.fn(({ setOpenAddIntegrationModal, setIsConnected, handleSlackAuthorization }) => (
|
||||
<div data-testid="manage-integration">
|
||||
<button onClick={() => setOpenAddIntegrationModal(true)}>Open Modal</button>
|
||||
<button onClick={() => setIsConnected(false)}>Disconnect</button>
|
||||
<button onClick={handleSlackAuthorization}>Reconnect</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/slack/components/ManageIntegration",
|
||||
() => ({
|
||||
ManageIntegration: vi.fn(({ setOpenAddIntegrationModal, setIsConnected, handleSlackAuthorization }) => (
|
||||
<div data-testid="manage-integration">
|
||||
<button onClick={() => setOpenAddIntegrationModal(true)}>Open Modal</button>
|
||||
<button onClick={() => setIsConnected(false)}>Disconnect</button>
|
||||
<button onClick={handleSlackAuthorization}>Reconnect</button>
|
||||
</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/lib/slack", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/slack/lib/slack", () => ({
|
||||
authorize: vi.fn(),
|
||||
}));
|
||||
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { getSlackChannelsAction } from "@/app/(app)/environments/[environmentId]/integrations/slack/actions";
|
||||
import { AddChannelMappingModal } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/AddChannelMappingModal";
|
||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/slack/lib/slack";
|
||||
import { getSlackChannelsAction } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/actions";
|
||||
import { AddChannelMappingModal } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/AddChannelMappingModal";
|
||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/ManageIntegration";
|
||||
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/lib/slack";
|
||||
import slackLogo from "@/images/slacklogo.png";
|
||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
@@ -32,7 +32,7 @@ export const SlackWrapper = ({
|
||||
}: SlackWrapperProps) => {
|
||||
const [isConnected, setIsConnected] = useState(slackIntegration ? slackIntegration.config?.key : false);
|
||||
const [slackChannels, setSlackChannels] = useState<TIntegrationItem[]>([]);
|
||||
const [isModalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const [showReconnectButton, setShowReconnectButton] = useState<boolean>(false);
|
||||
const [selectedIntegration, setSelectedIntegration] = useState<
|
||||
(TIntegrationSlackConfigData & { index: number }) | null
|
||||
@@ -72,7 +72,7 @@ export const SlackWrapper = ({
|
||||
environmentId={environment.id}
|
||||
surveys={surveys}
|
||||
open={isModalOpen}
|
||||
setOpen={setModalOpen}
|
||||
setOpen={setIsModalOpen}
|
||||
channels={slackChannels}
|
||||
slackIntegration={slackIntegration}
|
||||
selectedIntegration={selectedIntegration}
|
||||
@@ -80,7 +80,7 @@ export const SlackWrapper = ({
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
slackIntegration={slackIntegration}
|
||||
setOpenAddIntegrationModal={setModalOpen}
|
||||
setOpenAddIntegrationModal={setIsModalOpen}
|
||||
setIsConnected={setIsConnected}
|
||||
setSelectedIntegration={setSelectedIntegration}
|
||||
refreshChannels={getSlackChannels}
|
||||
+16
-13
@@ -1,6 +1,6 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/integrations/slack/page";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
|
||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/project/integrations/slack/page";
|
||||
import { getIntegrationByType } from "@/lib/integration/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
@@ -13,18 +13,21 @@ import { TIntegrationSlack, TIntegrationSlackCredential } from "@formbricks/type
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/lib/surveys", () => ({
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys", () => ({
|
||||
getSurveys: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper", () => ({
|
||||
SlackWrapper: vi.fn(({ isEnabled, environment, surveys, slackIntegration, webAppUrl, locale }) => (
|
||||
<div data-testid="slack-wrapper">
|
||||
Mock SlackWrapper: isEnabled={isEnabled.toString()}, envId={environment.id}, surveys=
|
||||
{surveys.length}, integrationId={slackIntegration?.id}, webAppUrl={webAppUrl}, locale={locale}
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
vi.mock(
|
||||
"@/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper",
|
||||
() => ({
|
||||
SlackWrapper: vi.fn(({ isEnabled, environment, surveys, slackIntegration, webAppUrl, locale }) => (
|
||||
<div data-testid="slack-wrapper">
|
||||
Mock SlackWrapper: isEnabled={isEnabled.toString()}, envId={environment.id}, surveys=
|
||||
{surveys.length}, integrationId={slackIntegration?.id}, webAppUrl={webAppUrl}, locale={locale}
|
||||
</div>
|
||||
)),
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_PRODUCTION: true,
|
||||
@@ -155,7 +158,7 @@ describe("SlackIntegrationPage", () => {
|
||||
"environments.integrations.slack.slack_integration"
|
||||
);
|
||||
expect(screen.getByTestId("go-back-button")).toHaveTextContent(
|
||||
`Go Back: http://test.formbricks.com/environments/${environmentId}/integrations`
|
||||
`Go Back: http://test.formbricks.com/environments/${environmentId}/project/integrations`
|
||||
);
|
||||
expect(screen.getByTestId("slack-wrapper")).toBeInTheDocument();
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
|
||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper";
|
||||
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants";
|
||||
import { getIntegrationByType } from "@/lib/integration/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
@@ -32,7 +32,7 @@ const Page = async (props) => {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/integrations`} />
|
||||
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/project/integrations`} />
|
||||
<PageHeader pageTitle={t("environments.integrations.slack.slack_integration")} />
|
||||
<div className="h-[75vh] w-full">
|
||||
<SlackWrapper
|
||||
+21
-7
@@ -2,9 +2,16 @@
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { H3, Small } from "@/modules/ui/components/typography";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { H4, Small } from "@/modules/ui/components/typography";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
|
||||
interface ButtonInfo {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
variant: "secondary" | "default" | "outline" | "ghost" | "link";
|
||||
}
|
||||
|
||||
export const SettingsCard = ({
|
||||
title,
|
||||
description,
|
||||
@@ -13,6 +20,7 @@ export const SettingsCard = ({
|
||||
noPadding = false,
|
||||
beta,
|
||||
className,
|
||||
buttonInfo,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -21,6 +29,7 @@ export const SettingsCard = ({
|
||||
noPadding?: boolean;
|
||||
beta?: boolean;
|
||||
className?: string;
|
||||
buttonInfo?: ButtonInfo;
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
@@ -30,19 +39,24 @@ export const SettingsCard = ({
|
||||
className
|
||||
)}
|
||||
id={title}>
|
||||
<div className="border-b border-slate-200 px-4 pb-4">
|
||||
<div className="flex">
|
||||
<H3 className="capitalize">{title}</H3>
|
||||
<div className="flex justify-between border-b border-slate-200 px-4 pb-4">
|
||||
<div>
|
||||
<H4 className="font-medium capitalize tracking-normal">{title}</H4>
|
||||
<div className="ml-2">
|
||||
{beta && <Badge size="normal" type="warning" text="Beta" />}
|
||||
{soon && (
|
||||
<Badge size="normal" type="success" text={t("environments.settings.enterprise.coming_soon")} />
|
||||
)}
|
||||
</div>
|
||||
<Small color="muted" margin="headerDescription">
|
||||
{description}
|
||||
</Small>
|
||||
</div>
|
||||
<Small color="muted" margin="headerDescription">
|
||||
{description}
|
||||
</Small>
|
||||
{buttonInfo && (
|
||||
<Button type="button" onClick={buttonInfo?.onClick} variant={buttonInfo?.variant ?? "default"}>
|
||||
{buttonInfo?.text}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn(noPadding ? "" : "px-4 pt-4")}>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -10,14 +10,7 @@ export const canUserAccessOrganization = async (userId: string, organizationId:
|
||||
|
||||
try {
|
||||
const userOrganizations = await getOrganizationsByUserId(userId);
|
||||
|
||||
const givenOrganizationExists = userOrganizations.filter(
|
||||
(organization) => organization.id === organizationId
|
||||
);
|
||||
if (!givenOrganizationExists) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return userOrganizations.some((organization) => organization.id === organizationId);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Kontoeinstellungen",
|
||||
"action": "Aktion",
|
||||
"actions": "Aktionen",
|
||||
"actions_description": "Code- und No-Code-Aktionen werden verwendet, um Abfangumfragen innerhalb von Apps und auf Websites auszulösen.",
|
||||
"active_surveys": "Aktive Umfragen",
|
||||
"activity": "Aktivität",
|
||||
"add": "Hinzufügen",
|
||||
@@ -790,6 +791,8 @@
|
||||
"receiving_data": "Daten werden empfangen \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Erneut prüfen",
|
||||
"scroll_to_the_top": "Scroll nach oben!",
|
||||
"setup_alert_description": "Befolge dieses Schritt-für-Schritt-Tutorial, um deine App oder Website in weniger als 5 Minuten zu verbinden.",
|
||||
"setup_alert_title": "Wie man verbindet",
|
||||
"step_1": "Schritt 1: Installiere mit pnpm, npm oder yarn",
|
||||
"step_2": "Schritt 2: Widget initialisieren",
|
||||
"step_2_description": "Importiere Formbricks und initialisiere das Widget in deiner Komponente (z.B. App.tsx):",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Account settings",
|
||||
"action": "Action",
|
||||
"actions": "Actions",
|
||||
"actions_description": "Code and No-Code Actions are used to trigger intercept surveys within apps & on websites.",
|
||||
"active_surveys": "Active surveys",
|
||||
"activity": "Activity",
|
||||
"add": "Add",
|
||||
@@ -790,6 +791,8 @@
|
||||
"receiving_data": "Receiving data \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Re-check",
|
||||
"scroll_to_the_top": "Scroll to the top!",
|
||||
"setup_alert_description": "Follow this step-by-step tutorial to connect your app or website in under 5 minutes.",
|
||||
"setup_alert_title": "How to connect",
|
||||
"step_1": "Step 1: Install with pnpm, npm or yarn",
|
||||
"step_2": "Step 2: Initialize widget",
|
||||
"step_2_description": "Import Formbricks and initialize the widget in your Component (e.g. App.tsx):",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Paramètres du compte",
|
||||
"action": "Action",
|
||||
"actions": "Actions",
|
||||
"actions_description": "Les actions avec ou sans code sont utilisées pour déclencher des enquêtes d'interception dans les applications et sur les sites Web.",
|
||||
"active_surveys": "Sondages actifs",
|
||||
"activity": "Activité",
|
||||
"add": "Ajouter",
|
||||
@@ -790,6 +791,8 @@
|
||||
"receiving_data": "Réception des données \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Re-vérifier",
|
||||
"scroll_to_the_top": "Faites défiler vers le haut !",
|
||||
"setup_alert_description": "Suivez ce tutoriel étape par étape pour connecter votre application ou site web en moins de 5 minutes.",
|
||||
"setup_alert_title": "Comment connecter",
|
||||
"step_1": "Étape 1 : Installer avec pnpm, npm ou yarn",
|
||||
"step_2": "Étape 2 : Initialiser le widget",
|
||||
"step_2_description": "Importez Formbricks et initialisez le widget dans votre composant (par exemple, App.tsx) :",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "アカウント設定",
|
||||
"action": "アクション",
|
||||
"actions": "アクション",
|
||||
"actions_description": "コードとノーコードアクションは、アプリ内やウェブサイト上で調査を発動するために使用されます。",
|
||||
"active_surveys": "アクティブなフォーム",
|
||||
"activity": "アクティビティ",
|
||||
"add": "追加",
|
||||
@@ -790,6 +791,8 @@
|
||||
"receiving_data": "データ受信中 \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "再チェック",
|
||||
"scroll_to_the_top": "ページ上部に戻る",
|
||||
"setup_alert_description": "5 分以内でアプリまたはウェブサイト を 接続する手順をステップバイステップ の チュートリアルに従ってください。",
|
||||
"setup_alert_title": "接続方法",
|
||||
"step_1": "ステップ1: pnpm / npm / yarnでインストール",
|
||||
"step_2": "ステップ2: ウィジェットの初期化",
|
||||
"step_2_description": "Formbricksをインポートし、コンポーネント(例: App.tsx)でウィジェットを初期化します。",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Configurações da conta",
|
||||
"action": "Ação",
|
||||
"actions": "Ações",
|
||||
"actions_description": "Ações de Código e Sem Código são usadas para acionar interceptar pesquisas dentro de apps & em sites.",
|
||||
"active_surveys": "Pesquisas ativas",
|
||||
"activity": "Atividade",
|
||||
"add": "Adicionar",
|
||||
@@ -790,6 +791,8 @@
|
||||
"receiving_data": "Recebendo dados \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Verificar novamente",
|
||||
"scroll_to_the_top": "Rola pra cima!",
|
||||
"setup_alert_description": "Siga este tutorial passo a passo para conectar seu app ou site em menos de 5 minutos.",
|
||||
"setup_alert_title": "Como conectar",
|
||||
"step_1": "Passo 1: Instale com pnpm, npm ou yarn",
|
||||
"step_2": "Passo 2: Iniciar widget",
|
||||
"step_2_description": "Importe o Formbricks e inicialize o widget no seu Componente (por exemplo, App.tsx):",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Configurações da conta",
|
||||
"action": "Ação",
|
||||
"actions": "Ações",
|
||||
"actions_description": "Ações com Código e Sem Código são usadas para acionar inquéritos de intercepção dentro de apps e em websites.",
|
||||
"active_surveys": "Inquéritos ativos",
|
||||
"activity": "Atividade",
|
||||
"add": "Adicionar",
|
||||
@@ -790,6 +791,8 @@
|
||||
"receiving_data": "A receber dados \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Verificar novamente",
|
||||
"scroll_to_the_top": "Rolar para o topo!",
|
||||
"setup_alert_description": "Siga este tutorial passo-a-passo para ligar a sua aplicação ou website em menos de 5 minutos",
|
||||
"setup_alert_title": "Como conectar",
|
||||
"step_1": "Passo 1: Instalar com pnpm, npm ou yarn",
|
||||
"step_2": "Passo 2: Inicializar widget",
|
||||
"step_2_description": "Importar Formbricks e inicializar o widget no seu Componente (por exemplo, App.tsx):",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "Setări cont",
|
||||
"action": "Acțiune",
|
||||
"actions": "Acțiuni",
|
||||
"actions_description": "Acțiunile Cod și No-Code sunt utilizate pentru a declanșa chestionare de interceptare în aplicații și pe site-uri web.",
|
||||
"active_surveys": "Sondaje active",
|
||||
"activity": "Activitate",
|
||||
"add": "Adaugă",
|
||||
@@ -790,6 +791,8 @@
|
||||
"receiving_data": "Recepționare date \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Re-verifică",
|
||||
"scroll_to_the_top": "Derulați în partea de sus!",
|
||||
"setup_alert_description": "Urmează acest tutorial pas cu pas pentru a-ți conecta aplicația sau site-ul în mai puțin de 5 minute.",
|
||||
"setup_alert_title": "Cum să conectezi",
|
||||
"step_1": "Pasul 1: Instalează cu pnpm, npm sau yarn",
|
||||
"step_2": "Pasul 2: Inițializează widget-ul",
|
||||
"step_2_description": "Importați Formbricks și inițializați widgetul în componenta dumneavoastră (de exemplu, App.tsx):",
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"account_settings": "帳戶設定",
|
||||
"action": "操作",
|
||||
"actions": "操作",
|
||||
"actions_description": "代碼 和 無代碼 動作 用於 觸發 截取 調查 於 應用程式 和 網站上 。",
|
||||
"active_surveys": "啟用中的問卷",
|
||||
"activity": "活動",
|
||||
"add": "新增",
|
||||
@@ -790,6 +791,8 @@
|
||||
"receiving_data": "正在接收資料 \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "重新檢查",
|
||||
"scroll_to_the_top": "捲動至頂端!",
|
||||
"setup_alert_description": "遵循 此 分步 教程 ,在 5 分鐘 內 將您的應用程式 或 網站 連線 。",
|
||||
"setup_alert_title": "如何 連線",
|
||||
"step_1": "步驟 1:使用 pnpm、npm 或 yarn 安裝",
|
||||
"step_2": "步驟 2:初始化小工具",
|
||||
"step_2_description": "匯入 Formbricks 並在您的元件中初始化小工具(例如,App.tsx):",
|
||||
|
||||
@@ -7,51 +7,49 @@ import { UsersIcon } from "lucide-react";
|
||||
const Loading = async () => {
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Contacts">
|
||||
<ContactsSecondaryNavigation activeId="segments" loading />
|
||||
</PageHeader>
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-7 content-center border-b text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-4 pl-6">{t("common.title")}</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">{t("common.surveys")}</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">{t("common.updated_at")}</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">{t("common.created_at")}</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="m-2 grid h-16 grid-cols-7 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<UsersIcon className="h-5 w-5 flex-shrink-0 animate-pulse text-slate-500" />
|
||||
<div className="flex flex-col">
|
||||
<div className="font-medium text-slate-900">
|
||||
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-900">
|
||||
<div className="h-2 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Contacts">
|
||||
<ContactsSecondaryNavigation activeId="segments" loading />
|
||||
</PageHeader>
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-7 content-center border-b text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-4 pl-6">{t("common.title")}</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">{t("common.surveys")}</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">{t("common.updated_at")}</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">{t("common.created_at")}</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={`${index.toString()}-segment-loading`}
|
||||
className="m-2 grid h-16 grid-cols-7 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<UsersIcon className="h-5 w-5 flex-shrink-0 animate-pulse text-slate-500" />
|
||||
<div className="flex flex-col">
|
||||
<div className="font-medium text-slate-900">
|
||||
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-900">
|
||||
<div className="h-2 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="whitespace-wrap col-span-1 my-auto text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="col-span-1 my-auto whitespace-normal text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="col-span-1 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="whitespace-wrap col-span-1 my-auto text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="col-span-1 my-auto whitespace-normal text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
</>
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export const AccessTable = ({ teams }: AccessTableProps) => {
|
||||
<TableBody className="[&_tr:last-child]:border-b">
|
||||
{teams.length === 0 && (
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={3} className="text-center">
|
||||
<TableCell colSpan={4} className="text-center">
|
||||
{t("environments.project.teams.no_teams_found")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -51,7 +51,6 @@ export const CreateOrganizationModal = ({ open, setOpen }: CreateOrganizationMod
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createOrganizationResponse);
|
||||
toast.error(errorMessage);
|
||||
console.error(errorMessage);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
||||
+2
-2
@@ -136,8 +136,8 @@ const getLatestStableFbRelease = async (): Promise<string | null> => {
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
} catch (error) {
|
||||
throw new Error("Failed to get latest stable Formbricks release", { cause: error });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,9 +46,28 @@ vi.mock("@/modules/projects/settings/(setup)/components/setup-instructions", ()
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/modules/projects/settings/(setup)/components/environment-id-field", () => ({
|
||||
EnvironmentIdField: ({ environmentId }: any) => (
|
||||
<div data-testid="environment-id-field">{environmentId}</div>
|
||||
|
||||
vi.mock("../components/action-settings-card", () => ({
|
||||
ActionSettingsCard: () => <div data-testid="action-settings-card">action-settings-card</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/alert", () => ({
|
||||
Alert: ({ children }: any) => <div data-testid="alert">{children}</div>,
|
||||
AlertButton: ({ children }: any) => <div data-testid="alert-button">{children}</div>,
|
||||
AlertDescription: ({ children }: any) => <div data-testid="alert-description">{children}</div>,
|
||||
AlertTitle: ({ children }: any) => <div data-testid="alert-title">{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/id-badge", () => ({
|
||||
IdBadge: ({ id }: any) => <div data-testid="id-badge">{id}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => ({
|
||||
__esModule: true,
|
||||
default: ({ children, href, target }: any) => (
|
||||
<a href={href} target={target} data-testid="link">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -57,15 +76,56 @@ vi.mock("@/tolgee/server", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(async (environmentId: string) => ({ environment: { id: environmentId } })),
|
||||
getEnvironmentAuth: vi.fn(async (environmentId: string) => ({
|
||||
environment: { id: environmentId, projectId: "project-123" },
|
||||
isReadOnly: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
let mockWebappUrl = "https://example.com";
|
||||
vi.mock("@/lib/environment/service", () => ({
|
||||
getEnvironments: vi.fn(async (projectId: string) => [
|
||||
{ id: "env-123", projectId },
|
||||
{ id: "env-456", projectId },
|
||||
]),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/actionClass/service", () => ({
|
||||
getActionClasses: vi.fn(async () => []),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/getPublicUrl", () => ({
|
||||
getPublicDomain: vi.fn(() => "https://example.com"),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/locale", () => ({
|
||||
findMatchingLocale: vi.fn(async () => "en"),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
get WEBAPP_URL() {
|
||||
return mockWebappUrl;
|
||||
},
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
SENTRY_RELEASE: "mock-sentry-release",
|
||||
SENTRY_ENVIRONMENT: "mock-sentry-environment",
|
||||
SESSION_MAX_AGE: 1000,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/env", () => ({
|
||||
@@ -87,16 +147,18 @@ describe("AppConnectionPage", () => {
|
||||
expect(await findByTestId("page-header")).toHaveTextContent("common.project_configuration");
|
||||
expect(await findByTestId("project-config-navigation")).toHaveTextContent("env-123 app-connection");
|
||||
expect(await findByTestId("environment-notice")).toHaveTextContent("env-123 /project/app-connection");
|
||||
|
||||
// Check that ActionSettingsCard is rendered
|
||||
expect(await findByTestId("action-settings-card")).toBeInTheDocument();
|
||||
expect(await findByTestId("action-settings-card")).toHaveTextContent("action-settings-card");
|
||||
|
||||
const cards = await findAllByTestId("settings-card");
|
||||
expect(cards.length).toBe(3);
|
||||
expect(cards[0]).toHaveTextContent("environments.project.app-connection.app_connection");
|
||||
expect(cards[0]).toHaveTextContent("environments.project.app-connection.app_connection_description");
|
||||
expect(cards[0]).toHaveTextContent("env-123"); // WidgetStatusIndicator
|
||||
expect(cards[1]).toHaveTextContent("environments.project.app-connection.how_to_setup");
|
||||
expect(cards[1]).toHaveTextContent("environments.project.app-connection.how_to_setup_description");
|
||||
expect(cards[1]).toHaveTextContent("env-123"); // SetupInstructions
|
||||
expect(cards[2]).toHaveTextContent("environments.project.app-connection.environment_id");
|
||||
expect(cards[2]).toHaveTextContent("environments.project.app-connection.environment_id_description");
|
||||
expect(cards[2]).toHaveTextContent("env-123"); // EnvironmentIdField
|
||||
expect(cards.length).toBe(2);
|
||||
expect(cards[0]).toHaveTextContent("environments.project.app-connection.environment_id");
|
||||
expect(cards[0]).toHaveTextContent("environments.project.app-connection.environment_id_description");
|
||||
expect(cards[0]).toHaveTextContent("env-123"); // IdBadge
|
||||
expect(cards[1]).toHaveTextContent("environments.project.app-connection.app_connection");
|
||||
expect(cards[1]).toHaveTextContent("environments.project.app-connection.app_connection_description");
|
||||
expect(cards[1]).toHaveTextContent("env-123"); // WidgetStatusIndicator
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,56 +1,90 @@
|
||||
"use server";
|
||||
|
||||
import { WidgetStatusIndicator } from "@/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator";
|
||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getActionClasses } from "@/lib/actionClass/service";
|
||||
import { getEnvironments } from "@/lib/environment/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { SetupInstructions } from "@/modules/projects/settings/(setup)/components/setup-instructions";
|
||||
import { ProjectConfigNavigation } from "@/modules/projects/settings/components/project-config-navigation";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Alert, AlertButton, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { EnvironmentNotice } from "@/modules/ui/components/environment-notice";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import Link from "next/link";
|
||||
import { ActionSettingsCard } from "../components/action-settings-card";
|
||||
|
||||
export const AppConnectionPage = async (props) => {
|
||||
const params = await props.params;
|
||||
export const AppConnectionPage = async ({ params }: { params: Promise<{ environmentId: string }> }) => {
|
||||
const t = await getTranslate();
|
||||
const { environmentId } = await params;
|
||||
|
||||
const { environment } = await getEnvironmentAuth(params.environmentId);
|
||||
const { environment, isReadOnly } = await getEnvironmentAuth(environmentId);
|
||||
|
||||
const [environments, actionClasses] = await Promise.all([
|
||||
getEnvironments(environment.projectId),
|
||||
getActionClasses(environmentId),
|
||||
]);
|
||||
const otherEnvironment = environments.filter((env) => env.id !== environmentId)[0];
|
||||
const [otherEnvActionClasses, locale] = await Promise.all([
|
||||
otherEnvironment ? getActionClasses(otherEnvironment.id) : Promise.resolve([]),
|
||||
findMatchingLocale(),
|
||||
]);
|
||||
|
||||
const publicDomain = getPublicDomain();
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.project_configuration")}>
|
||||
<ProjectConfigNavigation environmentId={params.environmentId} activeId="app-connection" />
|
||||
<ProjectConfigNavigation environmentId={environmentId} activeId="app-connection" />
|
||||
</PageHeader>
|
||||
<div className="space-y-4">
|
||||
<EnvironmentNotice environmentId={params.environmentId} subPageUrl="/project/app-connection" />
|
||||
<EnvironmentNotice environmentId={environmentId} subPageUrl="/project/app-connection" />
|
||||
<SettingsCard
|
||||
title={t("environments.project.app-connection.environment_id")}
|
||||
description={t("environments.project.app-connection.environment_id_description")}>
|
||||
<IdBadge id={environmentId} />
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title={t("environments.project.app-connection.app_connection")}
|
||||
description={t("environments.project.app-connection.app_connection_description")}>
|
||||
{environment && (
|
||||
<div className="space-y-4">
|
||||
<WidgetStatusIndicator environment={environment} />
|
||||
<Alert variant="info">
|
||||
<AlertTitle>{t("environments.project.app-connection.cache_update_delay_title")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("environments.project.app-connection.cache_update_delay_description")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
{!environment.appSetupCompleted ? (
|
||||
<Alert variant="outbound">
|
||||
<AlertTitle>{t("environments.project.app-connection.setup_alert_title")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("environments.project.app-connection.setup_alert_description")}
|
||||
</AlertDescription>
|
||||
<AlertButton asChild>
|
||||
<Link
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{t("common.learn_more")}
|
||||
</Link>
|
||||
</AlertButton>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert variant="warning">
|
||||
<AlertTitle>{t("environments.project.app-connection.cache_update_delay_title")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("environments.project.app-connection.cache_update_delay_description")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title={t("environments.project.app-connection.how_to_setup")}
|
||||
description={t("environments.project.app-connection.how_to_setup_description")}
|
||||
noPadding>
|
||||
<SetupInstructions environmentId={params.environmentId} publicDomain={publicDomain} />
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title={t("environments.project.app-connection.environment_id")}
|
||||
description={t("environments.project.app-connection.environment_id_description")}>
|
||||
<IdBadge id={params.environmentId} />
|
||||
</SettingsCard>
|
||||
<ActionSettingsCard
|
||||
environment={environment}
|
||||
otherEnvironment={otherEnvironment}
|
||||
otherEnvActionClasses={otherEnvActionClasses}
|
||||
environmentId={environmentId}
|
||||
actionClasses={actionClasses}
|
||||
isReadOnly={isReadOnly}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Code2Icon, MousePointerClickIcon } from "lucide-react";
|
||||
|
||||
export const ACTION_TYPE_ICON_LOOKUP = {
|
||||
code: <Code2Icon className="h-4 w-4" data-testid="code-icon" />,
|
||||
noCode: <MousePointerClickIcon className="h-4 w-4" data-testid="nocode-icon" />,
|
||||
};
|
||||
+34
-4
@@ -1,3 +1,4 @@
|
||||
import { getActiveInactiveSurveysAction } from "@/modules/projects/settings/(setup)/app-connection/actions";
|
||||
import { createActionClassAction } from "@/modules/survey/editor/actions";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
@@ -5,18 +6,43 @@ import toast from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { getActiveInactiveSurveysAction } from "../actions";
|
||||
import { ActionActivityTab } from "./ActionActivityTab";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/actions/utils", () => ({
|
||||
vi.mock("@/modules/projects/settings/(setup)/app-connection/utils", () => ({
|
||||
ACTION_TYPE_ICON_LOOKUP: {
|
||||
noCode: <div>NoCodeIcon</div>,
|
||||
automatic: <div>AutomaticIcon</div>,
|
||||
code: <div>CodeIcon</div>,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
IS_PRODUCTION: false,
|
||||
SENTRY_DSN: "mock-sentry-dsn",
|
||||
SENTRY_RELEASE: "mock-sentry-release",
|
||||
SENTRY_ENVIRONMENT: "mock-sentry-environment",
|
||||
SESSION_MAX_AGE: 1000,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/time", () => ({
|
||||
convertDateTimeStringShort: (dateString: string) => `formatted-${dateString}`,
|
||||
}));
|
||||
@@ -53,10 +79,14 @@ vi.mock("@/modules/ui/components/loading-spinner", () => ({
|
||||
LoadingSpinner: () => <div>LoadingSpinner</div>,
|
||||
}));
|
||||
|
||||
vi.mock("../actions", () => ({
|
||||
vi.mock("@/modules/projects/settings/(setup)/app-connection/actions", () => ({
|
||||
getActiveInactiveSurveysAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: { success: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
const mockActionClass = {
|
||||
id: "action1",
|
||||
createdAt: new Date("2023-01-01T10:00:00Z"),
|
||||
+2
-2
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
||||
import { convertDateTimeStringShort } from "@/lib/time";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||
import { getActiveInactiveSurveysAction } from "@/modules/projects/settings/(setup)/app-connection/actions";
|
||||
import { ACTION_TYPE_ICON_LOOKUP } from "@/modules/projects/settings/(setup)/app-connection/utils";
|
||||
import { createActionClassAction } from "@/modules/survey/editor/actions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ErrorComponent } from "@/modules/ui/components/error-component";
|
||||
@@ -14,7 +15,6 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { getActiveInactiveSurveysAction } from "../actions";
|
||||
|
||||
interface ActivityTabProps {
|
||||
actionClass: TActionClass;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user