mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-08 02:43:06 -05:00
Compare commits
172 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e82e0c87a4 | |||
| f2f2defd10 | |||
| 128e94e4cf | |||
| ee0ea7caa6 | |||
| 52dc64ffd2 | |||
| bec3fa2dbd | |||
| 18a3c4f0f7 | |||
| 6c61afec2f | |||
| ce4d9350e2 | |||
| 3e6f81268d | |||
| 8137de3c80 | |||
| bf0ad45697 | |||
| b1a4277ca8 | |||
| 1876c13f52 | |||
| 0623bb9ff5 | |||
| d37cddaa7e | |||
| 24f632f9ce | |||
| b041e3da86 | |||
| 8d91a3db62 | |||
| c05e3f192d | |||
| 5b61e00560 | |||
| bf592937f4 | |||
| 7ed7101ac1 | |||
| 2e926936fb | |||
| a386451e6e | |||
| f0bf111e7b | |||
| 8a57a5b74b | |||
| 434cb1d0d0 | |||
| 8bde75a9ff | |||
| 6b880f29cb | |||
| 969c9834e5 | |||
| 5e33b7c9a4 | |||
| 230ea744fa | |||
| fae1fb8f96 | |||
| eac35daed9 | |||
| 45accc1edb | |||
| 02ebe8e9f8 | |||
| cae859e326 | |||
| 5352d563b6 | |||
| 711f2bfe67 | |||
| 6fcb5d39a2 | |||
| 1ed9859ee7 | |||
| cd72a0a78d | |||
| 2b09795787 | |||
| 2451acb9bd | |||
| 14dcded91b | |||
| 46062f91cd | |||
| ffd4478184 | |||
| 69da1862fa | |||
| c11d3241ab | |||
| 3fb09a1a26 | |||
| 6efa449c10 | |||
| 34b94689ca | |||
| 901fac7e08 | |||
| 739c662863 | |||
| 535974ff8a | |||
| a8b97abe9a | |||
| 28103604b4 | |||
| b5a7e15386 | |||
| fec4746d5d | |||
| 175323e7d9 | |||
| 6130737d51 | |||
| bf10a8d0b2 | |||
| 612f8dceb8 | |||
| 0303f16db4 | |||
| 07635b160e | |||
| 9cfcffdb5e | |||
| 02264ffc5f | |||
| e489c6a346 | |||
| 7dde3edd8d | |||
| 4304a7efd6 | |||
| a89d598f8d | |||
| 6ff5af712f | |||
| 398ba79e7e | |||
| 5127de9de0 | |||
| 2bf7788a1b | |||
| ee8122778b | |||
| 8aaa7ed9c0 | |||
| bc7c8c5715 | |||
| ab1ea7a5ce | |||
| 4f749355e0 | |||
| 18b60ddd35 | |||
| 87f1b01c7a | |||
| 851ea0deb2 | |||
| 9abbbfdd35 | |||
| cefc2bdf60 | |||
| 78473bf3d0 | |||
| 15403c6a92 | |||
| 35b98863a4 | |||
| 65f5968fb1 | |||
| 2dfea4d72f | |||
| 990c0eee31 | |||
| 07f16b8a43 | |||
| bf556b0608 | |||
| 8b0766a46e | |||
| 1f995d6e25 | |||
| 975a4d57f8 | |||
| 69bd576fc5 | |||
| a2e4a3bbd7 | |||
| 281f854332 | |||
| 24496774a5 | |||
| aeaf3215b4 | |||
| f4c5162590 | |||
| dedb7389f0 | |||
| ff77118932 | |||
| 7aed1b84de | |||
| 79a773432a | |||
| d53869f1df | |||
| fc9ddb2b0d | |||
| 6fcb6863bd | |||
| b1cee91ad9 | |||
| 9d2e988c59 | |||
| 60bd5cbeff | |||
| b6a3a15379 | |||
| c68f214eff | |||
| c90ee84483 | |||
| dc1ee72594 | |||
| 924132287e | |||
| e6f347aa07 | |||
| 367bc23dd4 | |||
| a1a11b2bb8 | |||
| 31d2ea7444 | |||
| 0653c6a59f | |||
| b6d793e109 | |||
| 3da7129413 | |||
| 75fbb23190 | |||
| 439dd0b44e | |||
| 2556f5e15d | |||
| d361c334d3 | |||
| cc0eec3bf0 | |||
| a4d808b479 | |||
| 18ae1748d3 | |||
| 60f6ca9463 | |||
| 3404e0c494 | |||
| 83499ae552 | |||
| 2ac0c1eb07 | |||
| 54ede3015e | |||
| 1b4f05a062 | |||
| 197dbf5aa6 | |||
| aa27d242bb | |||
| 7ca52a7a93 | |||
| 4a48839d17 | |||
| 92bd9bdac7 | |||
| ad4b6f8b8c | |||
| 8de5079db3 | |||
| a60206dd44 | |||
| d66abdcdaf | |||
| 03fa41a911 | |||
| cab438e474 | |||
| a6dfe78c81 | |||
| e4d96f4379 | |||
| 581a66b4a9 | |||
| 5cf0c15812 | |||
| ebaa2d363c | |||
| 597ea40b75 | |||
| 3c39dcc2de | |||
| e8df1dbb35 | |||
| 84987ce557 | |||
| 784ed855d7 | |||
| 5a17d4144d | |||
| 65c9db86c6 | |||
| bc94d34d1e | |||
| 22be60a0ba | |||
| a384963863 | |||
| c067ae73bb | |||
| dc78a30cbe | |||
| 9c9ae8a3a2 | |||
| 29a08151aa | |||
| f42a8822a9 | |||
| a771ae189a | |||
| 029e069af6 | |||
| 81272b96e1 |
@@ -0,0 +1 @@
|
|||||||
|
{"sessionId":"f77248e2-8840-41c6-968b-c3b7d8a9e913","pid":49125,"acquiredAt":1776168010367}
|
||||||
@@ -32,12 +32,39 @@ CRON_SECRET=
|
|||||||
# Set the minimum log level(debug, info, warn, error, fatal)
|
# Set the minimum log level(debug, info, warn, error, fatal)
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# BullMQ workers require REDIS_URL (for example `redis://localhost:6379`) to be set.
|
||||||
|
# BullMQ worker startup is enabled by default outside tests. Set to 0 to disable.
|
||||||
|
# BULLMQ_WORKER_ENABLED=1
|
||||||
|
# Set to 1 on web/API pods that only enqueue jobs while a separate BullMQ worker deployment consumes them.
|
||||||
|
# BULLMQ_EXTERNAL_WORKER_ENABLED=0
|
||||||
|
|
||||||
|
# Number of BullMQ worker instances started per Formbricks server process.
|
||||||
|
# BULLMQ_WORKER_COUNT=1
|
||||||
|
|
||||||
|
# Number of concurrent jobs each BullMQ worker can process.
|
||||||
|
# BULLMQ_WORKER_CONCURRENCY=1
|
||||||
|
|
||||||
|
# Survey publish/close scheduling is configured with public build-time env vars because the editor UI
|
||||||
|
# also needs to render the selected execution time and timezone.
|
||||||
|
# NEXT_PUBLIC_SURVEY_SCHEDULING_TIME_ZONE=Europe/Berlin
|
||||||
|
# NEXT_PUBLIC_SURVEY_SCHEDULING_LOCAL_HOUR=0
|
||||||
|
# NEXT_PUBLIC_SURVEY_SCHEDULING_LOCAL_MINUTE=0
|
||||||
|
|
||||||
##############
|
##############
|
||||||
# DATABASE #
|
# DATABASE #
|
||||||
##############
|
##############
|
||||||
|
|
||||||
DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=public'
|
DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=public'
|
||||||
|
|
||||||
|
#################
|
||||||
|
# HUB (DEV) #
|
||||||
|
#################
|
||||||
|
# The dev stack (pnpm db:up / pnpm go) runs Formbricks Hub on port 8080.
|
||||||
|
# Set explicitly to avoid confusion; override as needed when using docker-compose.dev.yml.
|
||||||
|
HUB_API_KEY=dev-api-key
|
||||||
|
HUB_API_URL=http://localhost:8080
|
||||||
|
HUB_DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||||
|
|
||||||
################
|
################
|
||||||
# MAIL SETUP #
|
# MAIL SETUP #
|
||||||
################
|
################
|
||||||
@@ -269,5 +296,24 @@ REDIS_URL=redis://localhost:6379
|
|||||||
# AUDIT_LOG_GET_USER_IP=0
|
# AUDIT_LOG_GET_USER_IP=0
|
||||||
|
|
||||||
|
|
||||||
|
# Cube.js Analytics (optional — only needed for the analytics/dashboard feature)
|
||||||
|
# Required when running the Cube service (docker-compose.dev.yml). Generate with: openssl rand -hex 32
|
||||||
|
# Use the same value for CUBEJS_API_TOKEN so the client can authenticate.
|
||||||
|
# CUBEJS_API_SECRET=
|
||||||
|
# URL where the Cube.js instance is running
|
||||||
|
# CUBEJS_API_URL=http://localhost:4000
|
||||||
|
# API token sent with each Cube.js request; must match CUBEJS_API_SECRET when CUBEJS_DEV_MODE is off
|
||||||
|
# CUBEJS_API_TOKEN=
|
||||||
|
#
|
||||||
|
# Cube connects to the local Postgres service by default in docker-compose.dev.yml.
|
||||||
|
# Override these only if your Hub DB runs on a different host.
|
||||||
|
# CUBEJS_DB_HOST=postgres
|
||||||
|
# CUBEJS_DB_PORT=5432
|
||||||
|
# CUBEJS_DB_NAME=postgres
|
||||||
|
# CUBEJS_DB_USER=postgres
|
||||||
|
# CUBEJS_DB_PASS=postgres
|
||||||
|
#
|
||||||
|
# Alternative (when not on same Docker network): host.docker.internal and port 5433
|
||||||
|
|
||||||
# Lingo.dev API key for translation generation
|
# Lingo.dev API key for translation generation
|
||||||
LINGO_API_KEY=your_api_key_here
|
LINGO_API_KEY=your_api_key_here
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ The `@formbricks/surveys` package is pre-compiled (Vite → UMD + ESM) and the b
|
|||||||
|
|
||||||
TypeScript, React, and Prisma are the primary languages. Use the shared ESLint presets (`@formbricks/eslint-config`) and Prettier preset (110-char width, semicolons, double quotes, sorted import groups). Two-space indentation is standard; prefer `PascalCase` for React components and folders under `modules/`, `camelCase` for functions/variables, and `SCREAMING_SNAKE_CASE` only for constants. When adding mocks, place them inside `__mocks__` so import ordering stays stable.
|
TypeScript, React, and Prisma are the primary languages. Use the shared ESLint presets (`@formbricks/eslint-config`) and Prettier preset (110-char width, semicolons, double quotes, sorted import groups). Two-space indentation is standard; prefer `PascalCase` for React components and folders under `modules/`, `camelCase` for functions/variables, and `SCREAMING_SNAKE_CASE` only for constants. When adding mocks, place them inside `__mocks__` so import ordering stays stable.
|
||||||
We are using SonarQube to identify code smells and security hotspots.
|
We are using SonarQube to identify code smells and security hotspots.
|
||||||
|
Always mark React component props as `Readonly<>` (e.g., `({ children }: Readonly<MyProps>)`).
|
||||||
|
|
||||||
## Architecture & Patterns
|
## Architecture & Patterns
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"eslint-plugin-react-refresh": "0.4.26",
|
"eslint-plugin-react-refresh": "0.4.26",
|
||||||
"eslint-plugin-storybook": "10.2.17",
|
"eslint-plugin-storybook": "10.2.17",
|
||||||
"storybook": "10.2.17",
|
"storybook": "10.2.17",
|
||||||
"vite": "7.3.1",
|
"vite": "7.3.2",
|
||||||
"@storybook/addon-docs": "10.2.17"
|
"@storybook/addon-docs": "10.2.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -44,7 +44,7 @@ export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"z-40 flex w-sidebar-collapsed flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100"
|
"z-40 flex w-sidebar-collapsed flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100"
|
||||||
)}>
|
)}>
|
||||||
<Image src={FBLogo} width={160} height={30} alt={t("environments.formbricks_logo")} />
|
<Image src={FBLogo} width={160} height={30} alt={t("workspace.formbricks_logo")} />
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -105,7 +105,6 @@ export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
|
|||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
redirect: true,
|
redirect: true,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
clearEnvironmentId: true,
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { getEnvironments } from "@/lib/environment/service";
|
|
||||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
|
|
||||||
const LandingLayout = async (props: {
|
const LandingLayout = async (props: {
|
||||||
@@ -24,16 +23,11 @@ const LandingLayout = async (props: {
|
|||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||||
|
|
||||||
if (projects.length !== 0) {
|
if (workspaces.length !== 0) {
|
||||||
const firstProject = projects[0];
|
const firstWorkspace = workspaces[0];
|
||||||
const environments = await getEnvironments(firstProject.id);
|
return redirect(`/workspaces/${firstWorkspace.id}/`);
|
||||||
const prodEnvironment = environments.find((e) => e.type === "production");
|
|
||||||
|
|
||||||
if (prodEnvironment) {
|
|
||||||
return redirect(`/environments/${prodEnvironment.id}/`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
||||||
import { ProjectAndOrgSwitch } from "@/app/(app)/environments/[environmentId]/components/project-and-org-switch";
|
import { WorkspaceAndOrgSwitch } from "@/app/(app)/workspaces/[workspaceId]/components/workspace-and-org-switch";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
import { getAccessFlags } from "@/lib/membership/utils";
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
@@ -35,12 +35,12 @@ const Page = async (props: { params: Promise<{ organizationId: string }> }) => {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{/* we only need to render organization breadcrumb on this page, organizations/projects are lazy-loaded */}
|
{/* we only need to render organization breadcrumb on this page, organizations/workspaces are lazy-loaded */}
|
||||||
<ProjectAndOrgSwitch
|
<WorkspaceAndOrgSwitch
|
||||||
currentOrganizationId={organization.id}
|
currentOrganizationId={organization.id}
|
||||||
currentOrganizationName={organization.name}
|
currentOrganizationName={organization.name}
|
||||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||||
organizationProjectsLimit={0}
|
organizationWorkspacesLimit={0}
|
||||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||||
isLicenseActive={false}
|
isLicenseActive={false}
|
||||||
isOwnerOrManager={false}
|
isOwnerOrManager={false}
|
||||||
@@ -48,7 +48,6 @@ const Page = async (props: { params: Promise<{ organizationId: string }> }) => {
|
|||||||
isMember={isMember}
|
isMember={isMember}
|
||||||
isBilling={isBilling}
|
isBilling={isBilling}
|
||||||
isMembershipPending={isMembershipPending}
|
isMembershipPending={isMembershipPending}
|
||||||
environments={[]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full flex-col items-center justify-center space-y-12">
|
<div className="flex h-full flex-col items-center justify-center space-y-12">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { getTranslate } from "@/lingodotdev/server";
|
|||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||||
|
|
||||||
const ProjectOnboardingLayout = async (props: {
|
const WorkspaceOnboardingLayout = async (props: {
|
||||||
params: Promise<{ organizationId: string }>;
|
params: Promise<{ organizationId: string }>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -47,4 +47,4 @@ const ProjectOnboardingLayout = async (props: {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectOnboardingLayout;
|
export default WorkspaceOnboardingLayout;
|
||||||
|
|||||||
+3
-3
@@ -2,7 +2,7 @@ import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -39,7 +39,7 @@ const Page = async (props: ChannelPageProps) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||||
@@ -48,7 +48,7 @@ const Page = async (props: ChannelPageProps) => {
|
|||||||
subtitle={t("organizations.workspaces.new.channel.channel_select_subtitle")}
|
subtitle={t("organizations.workspaces.new.channel.channel_select_subtitle")}
|
||||||
/>
|
/>
|
||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{workspaces.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
+6
-6
@@ -4,10 +4,10 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
|||||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
import { getAccessFlags } from "@/lib/membership/utils";
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { getOrganization } from "@/lib/organization/service";
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
import { getOrganizationProjectsCount } from "@/lib/project/service";
|
import { getOrganizationWorkspacesCount } from "@/lib/workspace/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
import { getOrganizationWorkspacesLimit } from "@/modules/ee/license-check/lib/utils";
|
||||||
|
|
||||||
const OnboardingLayout = async (props: {
|
const OnboardingLayout = async (props: {
|
||||||
params: Promise<{ organizationId: string }>;
|
params: Promise<{ organizationId: string }>;
|
||||||
@@ -32,12 +32,12 @@ const OnboardingLayout = async (props: {
|
|||||||
throw new ResourceNotFoundError(t("common.organization"), params.organizationId);
|
throw new ResourceNotFoundError(t("common.organization"), params.organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [organizationProjectsLimit, organizationProjectsCount] = await Promise.all([
|
const [organizationWorkspacesLimit, organizationWorkspacesCount] = await Promise.all([
|
||||||
getOrganizationProjectsLimit(organization.id),
|
getOrganizationWorkspacesLimit(organization.id),
|
||||||
getOrganizationProjectsCount(organization.id),
|
getOrganizationWorkspacesCount(organization.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (organizationProjectsCount >= organizationProjectsLimit) {
|
if (organizationWorkspacesCount >= organizationWorkspacesLimit) {
|
||||||
return redirect(`/`);
|
return redirect(`/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -2,7 +2,7 @@ import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -39,13 +39,13 @@ const Page = async (props: ModePageProps) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||||
<Header title={t("organizations.workspaces.new.mode.what_are_you_here_for")} />
|
<Header title={t("organizations.workspaces.new.mode.what_are_you_here_for")} />
|
||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{workspaces.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
+2
-2
@@ -13,8 +13,8 @@ export const SelectPlanOnboarding = async ({ organizationId }: SelectPlanOnboard
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-8">
|
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-8">
|
||||||
<Header
|
<Header
|
||||||
title={t("environments.settings.billing.select_plan_header_title")}
|
title={t("workspace.settings.billing.select_plan_header_title")}
|
||||||
subtitle={t("environments.settings.billing.select_plan_header_subtitle")}
|
subtitle={t("workspace.settings.billing.select_plan_header_subtitle")}
|
||||||
/>
|
/>
|
||||||
<SelectPlanCard nextUrl={nextUrl} organizationId={organizationId} />
|
<SelectPlanCard nextUrl={nextUrl} organizationId={organizationId} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+35
-40
@@ -8,19 +8,19 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
TProjectConfigChannel,
|
TWorkspaceConfigChannel,
|
||||||
TProjectConfigIndustry,
|
TWorkspaceConfigIndustry,
|
||||||
TProjectMode,
|
TWorkspaceMode,
|
||||||
TProjectUpdateInput,
|
TWorkspaceUpdateInput,
|
||||||
ZProjectUpdateInput,
|
ZWorkspaceUpdateInput,
|
||||||
} from "@formbricks/types/project";
|
} from "@formbricks/types/workspace";
|
||||||
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
import { createWorkspaceAction } from "@/app/(app)/workspaces/[workspaceId]/actions";
|
||||||
import { previewSurvey } from "@/app/lib/templates";
|
import { previewSurvey } from "@/app/lib/templates";
|
||||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
|
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
|
||||||
import { buildStylingFromBrandColor } from "@/lib/styling/constants";
|
import { buildStylingFromBrandColor } from "@/lib/styling/constants";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
|
||||||
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
||||||
|
import { TOrganizationTeam } from "@/modules/ee/teams/workspace-teams/types/team";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { ColorPicker } from "@/modules/ui/components/color-picker";
|
import { ColorPicker } from "@/modules/ui/components/color-picker";
|
||||||
import {
|
import {
|
||||||
@@ -36,34 +36,34 @@ import { Input } from "@/modules/ui/components/input";
|
|||||||
import { MultiSelect } from "@/modules/ui/components/multi-select";
|
import { MultiSelect } from "@/modules/ui/components/multi-select";
|
||||||
import { SurveyInline } from "@/modules/ui/components/survey";
|
import { SurveyInline } from "@/modules/ui/components/survey";
|
||||||
|
|
||||||
interface ProjectSettingsProps {
|
interface WorkspaceSettingsProps {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
projectMode: TProjectMode;
|
workspaceMode: TWorkspaceMode;
|
||||||
channel: TProjectConfigChannel;
|
channel: TWorkspaceConfigChannel;
|
||||||
industry: TProjectConfigIndustry;
|
industry: TWorkspaceConfigIndustry;
|
||||||
defaultBrandColor: string;
|
defaultBrandColor: string;
|
||||||
organizationTeams: TOrganizationTeam[];
|
organizationTeams: TOrganizationTeam[];
|
||||||
isAccessControlAllowed: boolean;
|
isAccessControlAllowed: boolean;
|
||||||
userProjectsCount: number;
|
userWorkspacesCount: number;
|
||||||
publicDomain: string;
|
publicDomain: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectSettings = ({
|
export const WorkspaceSettings = ({
|
||||||
organizationId,
|
organizationId,
|
||||||
projectMode,
|
workspaceMode,
|
||||||
channel,
|
channel,
|
||||||
industry,
|
industry,
|
||||||
defaultBrandColor,
|
defaultBrandColor,
|
||||||
organizationTeams,
|
organizationTeams,
|
||||||
isAccessControlAllowed = false,
|
isAccessControlAllowed = false,
|
||||||
userProjectsCount,
|
userWorkspacesCount,
|
||||||
publicDomain,
|
publicDomain,
|
||||||
}: ProjectSettingsProps) => {
|
}: WorkspaceSettingsProps) => {
|
||||||
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
|
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const addProject = async (data: TProjectUpdateInput) => {
|
const addWorkspace = async (data: TWorkspaceUpdateInput) => {
|
||||||
try {
|
try {
|
||||||
// Build the full styling from the chosen brand color so all derived
|
// Build the full styling from the chosen brand color so all derived
|
||||||
// colours (question, button, input, option, progress, etc.) are persisted.
|
// colours (question, button, input, option, progress, etc.) are persisted.
|
||||||
@@ -71,7 +71,7 @@ export const ProjectSettings = ({
|
|||||||
// back to STYLE_DEFAULTS computed from the default brand (#64748b).
|
// back to STYLE_DEFAULTS computed from the default brand (#64748b).
|
||||||
const fullStyling = buildStylingFromBrandColor(data.styling?.brandColor?.light);
|
const fullStyling = buildStylingFromBrandColor(data.styling?.brandColor?.light);
|
||||||
|
|
||||||
const createProjectResponse = await createProjectAction({
|
const createWorkspaceResponse = await createWorkspaceAction({
|
||||||
organizationId,
|
organizationId,
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
@@ -81,26 +81,21 @@ export const ProjectSettings = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (createProjectResponse?.data) {
|
if (createWorkspaceResponse?.data) {
|
||||||
// get production environment
|
if (globalThis.window !== undefined) {
|
||||||
const productionEnvironment = createProjectResponse.data.environments.find(
|
// Remove filters when creating a new workspace
|
||||||
(environment) => environment.type === "production"
|
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||||
);
|
|
||||||
if (productionEnvironment) {
|
|
||||||
if (globalThis.window !== undefined) {
|
|
||||||
// Rmove filters when creating a new project
|
|
||||||
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const workspaceId = createWorkspaceResponse.data.id;
|
||||||
if (channel === "app" || channel === "website") {
|
if (channel === "app" || channel === "website") {
|
||||||
router.push(`/environments/${productionEnvironment?.id}/connect`);
|
router.push(`/workspaces/${workspaceId}/connect`);
|
||||||
} else if (channel === "link") {
|
} else if (channel === "link") {
|
||||||
router.push(`/environments/${productionEnvironment?.id}/surveys`);
|
router.push(`/workspaces/${workspaceId}/surveys`);
|
||||||
} else if (projectMode === "cx") {
|
} else if (workspaceMode === "cx") {
|
||||||
router.push(`/environments/${productionEnvironment?.id}/xm-templates`);
|
router.push(`/workspaces/${workspaceId}/xm-templates`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = getFormattedErrorMessage(createProjectResponse);
|
const errorMessage = getFormattedErrorMessage(createWorkspaceResponse);
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -109,15 +104,15 @@ export const ProjectSettings = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const form = useForm<TProjectUpdateInput>({
|
const form = useForm<TWorkspaceUpdateInput>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
|
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
|
||||||
teamIds: [],
|
teamIds: [],
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZProjectUpdateInput),
|
resolver: zodResolver(ZWorkspaceUpdateInput),
|
||||||
});
|
});
|
||||||
const projectName = form.watch("name");
|
const workspaceName = form.watch("name");
|
||||||
const logoUrl = form.watch("logo.url");
|
const logoUrl = form.watch("logo.url");
|
||||||
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
|
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
|
||||||
const previewStyling = useMemo(() => buildStylingFromBrandColor(brandColor), [brandColor]);
|
const previewStyling = useMemo(() => buildStylingFromBrandColor(brandColor), [brandColor]);
|
||||||
@@ -132,7 +127,7 @@ export const ProjectSettings = ({
|
|||||||
<div className="mt-6 flex w-5/6 space-x-10 lg:w-2/3 2xl:w-1/2">
|
<div className="mt-6 flex w-5/6 space-x-10 lg:w-2/3 2xl:w-1/2">
|
||||||
<div className="flex w-1/2 flex-col space-y-4">
|
<div className="flex w-1/2 flex-col space-y-4">
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<form onSubmit={form.handleSubmit(addProject)} className="w-full space-y-4">
|
<form onSubmit={form.handleSubmit(addWorkspace)} className="w-full space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="styling.brandColor.light"
|
name="styling.brandColor.light"
|
||||||
@@ -184,7 +179,7 @@ export const ProjectSettings = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isAccessControlAllowed && userProjectsCount > 0 && (
|
{isAccessControlAllowed && userWorkspacesCount > 0 && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="teamIds"
|
name="teamIds"
|
||||||
@@ -242,7 +237,7 @@ export const ProjectSettings = ({
|
|||||||
<SurveyInline
|
<SurveyInline
|
||||||
appUrl={publicDomain}
|
appUrl={publicDomain}
|
||||||
isPreviewMode={true}
|
isPreviewMode={true}
|
||||||
survey={previewSurvey(projectName || t("common.my_product"), t)}
|
survey={previewSurvey(workspaceName || t("common.my_product"), t)}
|
||||||
styling={previewStyling}
|
styling={previewStyling}
|
||||||
isBrandingEnabled={false}
|
isBrandingEnabled={false}
|
||||||
languageCode="default"
|
languageCode="default"
|
||||||
+17
-13
@@ -2,30 +2,34 @@ import { XIcon } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
import {
|
||||||
|
TWorkspaceConfigChannel,
|
||||||
|
TWorkspaceConfigIndustry,
|
||||||
|
TWorkspaceMode,
|
||||||
|
} from "@formbricks/types/workspace";
|
||||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/settings/components/ProjectSettings";
|
import { WorkspaceSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/settings/components/WorkspaceSettings";
|
||||||
import { DEFAULT_BRAND_COLOR } from "@/lib/constants";
|
import { DEFAULT_BRAND_COLOR } from "@/lib/constants";
|
||||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
import { getUserWorkspaces } from "@/lib/workspace/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
|
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
|
|
||||||
interface ProjectSettingsPageProps {
|
interface WorkspaceSettingsPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
}>;
|
}>;
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
channel?: TProjectConfigChannel;
|
channel?: TWorkspaceConfigChannel;
|
||||||
industry?: TProjectConfigIndustry;
|
industry?: TWorkspaceConfigIndustry;
|
||||||
mode?: TProjectMode;
|
mode?: TWorkspaceMode;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Page = async (props: ProjectSettingsPageProps) => {
|
const Page = async (props: WorkspaceSettingsPageProps) => {
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
@@ -39,7 +43,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
|||||||
const channel = searchParams.channel ?? null;
|
const channel = searchParams.channel ?? null;
|
||||||
const industry = searchParams.industry ?? null;
|
const industry = searchParams.industry ?? null;
|
||||||
const mode = searchParams.mode ?? "surveys";
|
const mode = searchParams.mode ?? "surveys";
|
||||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
const workspaces = await getUserWorkspaces(session.user.id, params.organizationId);
|
||||||
|
|
||||||
const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
|
const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
|
||||||
|
|
||||||
@@ -57,18 +61,18 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
|||||||
title={t("organizations.workspaces.new.settings.workspace_settings_title")}
|
title={t("organizations.workspaces.new.settings.workspace_settings_title")}
|
||||||
subtitle={t("organizations.workspaces.new.settings.workspace_settings_subtitle")}
|
subtitle={t("organizations.workspaces.new.settings.workspace_settings_subtitle")}
|
||||||
/>
|
/>
|
||||||
<ProjectSettings
|
<WorkspaceSettings
|
||||||
organizationId={params.organizationId}
|
organizationId={params.organizationId}
|
||||||
projectMode={mode}
|
workspaceMode={mode}
|
||||||
channel={channel}
|
channel={channel}
|
||||||
industry={industry}
|
industry={industry}
|
||||||
defaultBrandColor={DEFAULT_BRAND_COLOR}
|
defaultBrandColor={DEFAULT_BRAND_COLOR}
|
||||||
organizationTeams={organizationTeams}
|
organizationTeams={organizationTeams}
|
||||||
isAccessControlAllowed={isAccessControlAllowed}
|
isAccessControlAllowed={isAccessControlAllowed}
|
||||||
userProjectsCount={projects.length}
|
userWorkspacesCount={workspaces.length}
|
||||||
publicDomain={publicDomain}
|
publicDomain={publicDomain}
|
||||||
/>
|
/>
|
||||||
{projects.length >= 1 && (
|
{workspaces.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
+10
-13
@@ -4,21 +4,20 @@ import { ArrowRight } from "lucide-react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TWorkspaceConfigChannel } from "@formbricks/types/workspace";
|
||||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
||||||
|
|
||||||
interface ConnectWithFormbricksProps {
|
interface ConnectWithFormbricksProps {
|
||||||
environment: TEnvironment;
|
workspaceId: string;
|
||||||
publicDomain: string;
|
publicDomain: string;
|
||||||
appSetupCompleted: boolean;
|
appSetupCompleted: boolean;
|
||||||
channel: TProjectConfigChannel;
|
channel: TWorkspaceConfigChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectWithFormbricks = ({
|
export const ConnectWithFormbricks = ({
|
||||||
environment,
|
workspaceId,
|
||||||
publicDomain,
|
publicDomain,
|
||||||
appSetupCompleted,
|
appSetupCompleted,
|
||||||
channel,
|
channel,
|
||||||
@@ -26,7 +25,7 @@ export const ConnectWithFormbricks = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const handleFinishOnboarding = async () => {
|
const handleFinishOnboarding = async () => {
|
||||||
router.push(`/environments/${environment.id}/surveys`);
|
router.push(`/workspaces/${workspaceId}/surveys`);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -48,7 +47,7 @@ export const ConnectWithFormbricks = ({
|
|||||||
<div className="flex w-full space-x-10">
|
<div className="flex w-full space-x-10">
|
||||||
<div className="flex w-1/2 flex-col space-y-4">
|
<div className="flex w-1/2 flex-col space-y-4">
|
||||||
<OnboardingSetupInstructions
|
<OnboardingSetupInstructions
|
||||||
environmentId={environment.id}
|
workspaceId={workspaceId}
|
||||||
publicDomain={publicDomain}
|
publicDomain={publicDomain}
|
||||||
channel={channel}
|
channel={channel}
|
||||||
appSetupCompleted={appSetupCompleted}
|
appSetupCompleted={appSetupCompleted}
|
||||||
@@ -61,9 +60,9 @@ export const ConnectWithFormbricks = ({
|
|||||||
)}>
|
)}>
|
||||||
{appSetupCompleted ? (
|
{appSetupCompleted ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl">{t("environments.connect.congrats")}</p>
|
<p className="text-3xl">{t("workspace.connect.congrats")}</p>
|
||||||
<p className="pt-4 text-sm font-medium text-slate-600">
|
<p className="pt-4 text-sm font-medium text-slate-600">
|
||||||
{t("environments.connect.connection_successful_message")}
|
{t("workspace.connect.connection_successful_message")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -73,7 +72,7 @@ export const ConnectWithFormbricks = ({
|
|||||||
<span className="relative inline-flex h-10 w-10 rounded-full bg-slate-500"></span>
|
<span className="relative inline-flex h-10 w-10 rounded-full bg-slate-500"></span>
|
||||||
</span>
|
</span>
|
||||||
<p className="pt-4 text-sm font-medium text-slate-600">
|
<p className="pt-4 text-sm font-medium text-slate-600">
|
||||||
{t("environments.connect.waiting_for_your_signal")}
|
{t("workspace.connect.waiting_for_your_signal")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -83,9 +82,7 @@ export const ConnectWithFormbricks = ({
|
|||||||
id="finishOnboarding"
|
id="finishOnboarding"
|
||||||
variant={appSetupCompleted ? "default" : "ghost"}
|
variant={appSetupCompleted ? "default" : "ghost"}
|
||||||
onClick={handleFinishOnboarding}>
|
onClick={handleFinishOnboarding}>
|
||||||
{appSetupCompleted
|
{appSetupCompleted ? t("workspace.connect.finish_onboarding") : t("workspace.connect.do_it_later")}
|
||||||
? t("environments.connect.finish_onboarding")
|
|
||||||
: t("environments.connect.do_it_later")}
|
|
||||||
<ArrowRight />
|
<ArrowRight />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
+19
-19
@@ -5,7 +5,7 @@ import "prismjs/themes/prism.css";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
import { TWorkspaceConfigChannel } from "@formbricks/types/workspace";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { CodeBlock } from "@/modules/ui/components/code-block";
|
import { CodeBlock } from "@/modules/ui/components/code-block";
|
||||||
import { Html5Icon, NpmIcon } from "@/modules/ui/components/icons";
|
import { Html5Icon, NpmIcon } from "@/modules/ui/components/icons";
|
||||||
@@ -17,14 +17,14 @@ const tabs = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
interface OnboardingSetupInstructionsProps {
|
interface OnboardingSetupInstructionsProps {
|
||||||
environmentId: string;
|
workspaceId: string;
|
||||||
publicDomain: string;
|
publicDomain: string;
|
||||||
channel: TProjectConfigChannel;
|
channel: TWorkspaceConfigChannel;
|
||||||
appSetupCompleted: boolean;
|
appSetupCompleted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OnboardingSetupInstructions = ({
|
export const OnboardingSetupInstructions = ({
|
||||||
environmentId,
|
workspaceId,
|
||||||
publicDomain,
|
publicDomain,
|
||||||
channel,
|
channel,
|
||||||
appSetupCompleted,
|
appSetupCompleted,
|
||||||
@@ -35,8 +35,8 @@ export const OnboardingSetupInstructions = ({
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
!function(){
|
!function(){
|
||||||
var appUrl = "${publicDomain}";
|
var appUrl = "${publicDomain}";
|
||||||
var environmentId = "${environmentId}";
|
var workspaceId = "${workspaceId}";
|
||||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:environmentId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({workspaceId:workspaceId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||||
</script>
|
</script>
|
||||||
<!-- END Formbricks Surveys -->
|
<!-- END Formbricks Surveys -->
|
||||||
`;
|
`;
|
||||||
@@ -45,46 +45,46 @@ export const OnboardingSetupInstructions = ({
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
!function(){
|
!function(){
|
||||||
var appUrl = "${publicDomain}";
|
var appUrl = "${publicDomain}";
|
||||||
var environmentId = "${environmentId}";
|
var workspaceId = "${workspaceId}";
|
||||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:environmentId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({workspaceId:workspaceId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||||
</script>
|
</script>
|
||||||
<!-- END Formbricks Surveys -->
|
<!-- END Formbricks Surveys -->
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const npmSnippetForAppSurveys = `
|
const npmSnippetForAppSurveys = `
|
||||||
import formbricks from "@formbricks/js";
|
import formbricks from "@formbricks/js";
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
formbricks.setup({
|
formbricks.setup({
|
||||||
environmentId: "${environmentId}",
|
workspaceId: "${workspaceId}",
|
||||||
appUrl: "${publicDomain}",
|
appUrl: "${publicDomain}",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// your own app
|
// your own app
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const npmSnippetForWebsiteSurveys = `
|
const npmSnippetForWebsiteSurveys = `
|
||||||
// other imports
|
// other imports
|
||||||
import formbricks from "@formbricks/js";
|
import formbricks from "@formbricks/js";
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
formbricks.setup({
|
formbricks.setup({
|
||||||
environmentId: "${environmentId}",
|
workspaceId: "${workspaceId}",
|
||||||
appUrl: "${publicDomain}",
|
appUrl: "${publicDomain}",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// your own app
|
// your own app
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -109,7 +109,7 @@ export const OnboardingSetupInstructions = ({
|
|||||||
yarn add @formbricks/js
|
yarn add @formbricks/js
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
<p className="text-sm text-slate-700">
|
<p className="text-sm text-slate-700">
|
||||||
{t("environments.connect.import_formbricks_and_initialize_the_widget_in_your_component")}
|
{t("workspace.connect.import_formbricks_and_initialize_the_widget_in_your_component")}
|
||||||
</p>
|
</p>
|
||||||
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
|
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
|
||||||
{channel === "app" ? npmSnippetForAppSurveys : npmSnippetForWebsiteSurveys}
|
{channel === "app" ? npmSnippetForAppSurveys : npmSnippetForWebsiteSurveys}
|
||||||
@@ -126,7 +126,7 @@ export const OnboardingSetupInstructions = ({
|
|||||||
) : activeTab === "html" ? (
|
) : activeTab === "html" ? (
|
||||||
<div className="prose prose-slate">
|
<div className="prose prose-slate">
|
||||||
<p className="-mb-1 mt-6 text-sm text-slate-700">
|
<p className="-mb-1 mt-6 text-sm text-slate-700">
|
||||||
{t("environments.connect.insert_this_code_into_the_head_tag_of_your_website")}
|
{t("workspace.connect.insert_this_code_into_the_head_tag_of_your_website")}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
|
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
|
||||||
+11
-17
@@ -1,56 +1,50 @@
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/workspaces/[workspaceId]/connect/components/ConnectWithFormbricks";
|
||||||
import { getEnvironment } from "@/lib/environment/service";
|
|
||||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
import { getWorkspace } from "@/lib/workspace/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
|
|
||||||
interface ConnectPageProps {
|
interface ConnectPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
environmentId: string;
|
workspaceId: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Page = async (props: ConnectPageProps) => {
|
const Page = async (props: ConnectPageProps) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const environment = await getEnvironment(params.environmentId);
|
|
||||||
|
|
||||||
if (!environment) {
|
const workspace = await getWorkspace(params.workspaceId);
|
||||||
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
|
if (!workspace) {
|
||||||
|
throw new ResourceNotFoundError(t("common.workspace"), params.workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await getProjectByEnvironmentId(environment.id);
|
const channel = workspace.config.channel || null;
|
||||||
if (!project) {
|
|
||||||
throw new ResourceNotFoundError(t("common.workspace"), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const channel = project.config.channel || null;
|
|
||||||
|
|
||||||
const publicDomain = getPublicDomain();
|
const publicDomain = getPublicDomain();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-full flex-col items-center justify-center py-10">
|
<div className="flex min-h-full flex-col items-center justify-center py-10">
|
||||||
<Header title={t("environments.connect.headline")} subtitle={t("environments.connect.subtitle")} />
|
<Header title={t("workspace.connect.headline")} subtitle={t("workspace.connect.subtitle")} />
|
||||||
<div className="space-y-4 text-center">
|
<div className="space-y-4 text-center">
|
||||||
<p className="text-4xl font-medium text-slate-800"></p>
|
<p className="text-4xl font-medium text-slate-800"></p>
|
||||||
<p className="text-sm text-slate-500"></p>
|
<p className="text-sm text-slate-500"></p>
|
||||||
</div>
|
</div>
|
||||||
<ConnectWithFormbricks
|
<ConnectWithFormbricks
|
||||||
environment={environment}
|
workspaceId={params.workspaceId}
|
||||||
publicDomain={publicDomain}
|
publicDomain={publicDomain}
|
||||||
appSetupCompleted={environment.appSetupCompleted}
|
appSetupCompleted={workspace.appSetupCompleted}
|
||||||
channel={channel}
|
channel={channel}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}`}>
|
<Link href={`/workspaces/${params.workspaceId}`}>
|
||||||
<XIcon className="h-7 w-7" strokeWidth={1.5} />
|
<XIcon className="h-7 w-7" strokeWidth={1.5} />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
import { AuthorizationError } from "@formbricks/types/errors";
|
||||||
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
import { hasUserWorkspaceAccess } from "@/lib/workspace/auth";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
|
|
||||||
const OnboardingLayout = async (props: {
|
const OnboardingLayout = async (props: {
|
||||||
params: Promise<{ environmentId: string }>;
|
params: Promise<{ workspaceId: string }>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
@@ -17,9 +17,9 @@ const OnboardingLayout = async (props: {
|
|||||||
return redirect(`/auth/login`);
|
return redirect(`/auth/login`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
|
const isAuthorized = await hasUserWorkspaceAccess(session.user.id, params.workspaceId);
|
||||||
if (!isAuthorized) {
|
if (!isAuthorized) {
|
||||||
throw new AuthorizationError("User is not authorized to access this environment");
|
throw new AuthorizationError("User is not authorized to access this workspace");
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="flex-1 bg-slate-50">{children}</div>;
|
return <div className="flex-1 bg-slate-50">{children}</div>;
|
||||||
+21
-21
@@ -5,23 +5,23 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TProject } from "@formbricks/types/project";
|
|
||||||
import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
|
import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
|
||||||
import { TXMTemplate } from "@formbricks/types/templates";
|
import { TXMTemplate } from "@formbricks/types/templates";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import { replacePresetPlaceholders } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils";
|
import { TWorkspace } from "@formbricks/types/workspace";
|
||||||
import { getXMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates";
|
|
||||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||||
|
import { replacePresetPlaceholders } from "@/app/(app)/(onboarding)/workspaces/[workspaceId]/xm-templates/lib/utils";
|
||||||
|
import { getXMTemplates } from "@/app/(app)/(onboarding)/workspaces/[workspaceId]/xm-templates/lib/xm-templates";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { createSurveyAction } from "@/modules/survey/components/template-list/actions";
|
import { createSurveyAction } from "@/modules/survey/components/template-list/actions";
|
||||||
|
|
||||||
interface XMTemplateListProps {
|
interface XMTemplateListProps {
|
||||||
project: TProject;
|
workspace: TWorkspace;
|
||||||
user: TUser;
|
user: TUser;
|
||||||
environmentId: string;
|
workspaceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListProps) => {
|
export const XMTemplateList = ({ workspace, user, workspaceId }: XMTemplateListProps) => {
|
||||||
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
|
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -33,12 +33,12 @@ export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListP
|
|||||||
createdBy: user.id,
|
createdBy: user.id,
|
||||||
};
|
};
|
||||||
const createSurveyResponse = await createSurveyAction({
|
const createSurveyResponse = await createSurveyAction({
|
||||||
environmentId: environmentId,
|
workspaceId: workspaceId,
|
||||||
surveyBody: augmentedTemplate,
|
surveyBody: augmentedTemplate,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (createSurveyResponse?.data) {
|
if (createSurveyResponse?.data) {
|
||||||
router.push(`/environments/${environmentId}/surveys/${createSurveyResponse.data.id}/edit?mode=cx`);
|
router.push(`/workspaces/${workspaceId}/surveys/${createSurveyResponse.data.id}/edit?mode=cx`);
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = getFormattedErrorMessage(createSurveyResponse);
|
const errorMessage = getFormattedErrorMessage(createSurveyResponse);
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
@@ -48,49 +48,49 @@ export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListP
|
|||||||
const handleTemplateClick = (templateIdx: number) => {
|
const handleTemplateClick = (templateIdx: number) => {
|
||||||
setActiveTemplateId(templateIdx);
|
setActiveTemplateId(templateIdx);
|
||||||
const template = getXMTemplates(t)[templateIdx];
|
const template = getXMTemplates(t)[templateIdx];
|
||||||
const newTemplate = replacePresetPlaceholders(template, project);
|
const newTemplate = replacePresetPlaceholders(template, workspace);
|
||||||
createSurvey(newTemplate);
|
createSurvey(newTemplate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const XMTemplateOptions = [
|
const XMTemplateOptions = [
|
||||||
{
|
{
|
||||||
title: t("environments.xm-templates.nps"),
|
title: t("workspace.xm-templates.nps"),
|
||||||
description: t("environments.xm-templates.nps_description"),
|
description: t("workspace.xm-templates.nps_description"),
|
||||||
icon: ShoppingCartIcon,
|
icon: ShoppingCartIcon,
|
||||||
onClick: () => handleTemplateClick(0),
|
onClick: () => handleTemplateClick(0),
|
||||||
isLoading: activeTemplateId === 0,
|
isLoading: activeTemplateId === 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("environments.xm-templates.five_star_rating"),
|
title: t("workspace.xm-templates.five_star_rating"),
|
||||||
description: t("environments.xm-templates.five_star_rating_description"),
|
description: t("workspace.xm-templates.five_star_rating_description"),
|
||||||
icon: StarIcon,
|
icon: StarIcon,
|
||||||
onClick: () => handleTemplateClick(1),
|
onClick: () => handleTemplateClick(1),
|
||||||
isLoading: activeTemplateId === 1,
|
isLoading: activeTemplateId === 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("environments.xm-templates.csat"),
|
title: t("workspace.xm-templates.csat"),
|
||||||
description: t("environments.xm-templates.csat_description"),
|
description: t("workspace.xm-templates.csat_description"),
|
||||||
icon: ThumbsUpIcon,
|
icon: ThumbsUpIcon,
|
||||||
onClick: () => handleTemplateClick(2),
|
onClick: () => handleTemplateClick(2),
|
||||||
isLoading: activeTemplateId === 2,
|
isLoading: activeTemplateId === 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("environments.xm-templates.ces"),
|
title: t("workspace.xm-templates.ces"),
|
||||||
description: t("environments.xm-templates.ces_description"),
|
description: t("workspace.xm-templates.ces_description"),
|
||||||
icon: ActivityIcon,
|
icon: ActivityIcon,
|
||||||
onClick: () => handleTemplateClick(3),
|
onClick: () => handleTemplateClick(3),
|
||||||
isLoading: activeTemplateId === 3,
|
isLoading: activeTemplateId === 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("environments.xm-templates.smileys"),
|
title: t("workspace.xm-templates.smileys"),
|
||||||
description: t("environments.xm-templates.smileys_description"),
|
description: t("workspace.xm-templates.smileys_description"),
|
||||||
icon: SmileIcon,
|
icon: SmileIcon,
|
||||||
onClick: () => handleTemplateClick(4),
|
onClick: () => handleTemplateClick(4),
|
||||||
isLoading: activeTemplateId === 4,
|
isLoading: activeTemplateId === 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("environments.xm-templates.enps"),
|
title: t("workspace.xm-templates.enps"),
|
||||||
description: t("environments.xm-templates.enps_description"),
|
description: t("workspace.xm-templates.enps_description"),
|
||||||
icon: UsersIcon,
|
icon: UsersIcon,
|
||||||
onClick: () => handleTemplateClick(5),
|
onClick: () => handleTemplateClick(5),
|
||||||
isLoading: activeTemplateId === 5,
|
isLoading: activeTemplateId === 5,
|
||||||
+14
-14
@@ -1,17 +1,17 @@
|
|||||||
import "@testing-library/jest-dom/vitest";
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { cleanup } from "@testing-library/react";
|
import { cleanup } from "@testing-library/react";
|
||||||
import { afterEach, describe, expect, test } from "vitest";
|
import { afterEach, describe, expect, test } from "vitest";
|
||||||
import { TProject } from "@formbricks/types/project";
|
|
||||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||||
import { TXMTemplate } from "@formbricks/types/templates";
|
import { TXMTemplate } from "@formbricks/types/templates";
|
||||||
|
import { TWorkspace } from "@formbricks/types/workspace";
|
||||||
import { replacePresetPlaceholders } from "./utils";
|
import { replacePresetPlaceholders } from "./utils";
|
||||||
|
|
||||||
// Mock data
|
// Mock data
|
||||||
const mockProject: TProject = {
|
const mockWorkspace: TWorkspace = {
|
||||||
id: "project1",
|
id: "workspace1",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: "Test Project",
|
name: "Test Workspace",
|
||||||
organizationId: "org1",
|
organizationId: "org1",
|
||||||
styling: {
|
styling: {
|
||||||
allowStyleOverwrite: true,
|
allowStyleOverwrite: true,
|
||||||
@@ -27,12 +27,12 @@ const mockProject: TProject = {
|
|||||||
placement: "bottomRight",
|
placement: "bottomRight",
|
||||||
clickOutsideClose: true,
|
clickOutsideClose: true,
|
||||||
overlay: "none",
|
overlay: "none",
|
||||||
environments: [],
|
appSetupCompleted: false,
|
||||||
languages: [],
|
languages: [],
|
||||||
logo: null,
|
logo: null,
|
||||||
};
|
};
|
||||||
const mockTemplate: TXMTemplate = {
|
const mockTemplate: TXMTemplate = {
|
||||||
name: "$[projectName] Survey",
|
name: "$[workspaceName] Survey",
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
id: "block1",
|
id: "block1",
|
||||||
@@ -42,7 +42,7 @@ const mockTemplate: TXMTemplate = {
|
|||||||
id: "q1",
|
id: "q1",
|
||||||
type: "openText" as TSurveyElementTypeEnum.OpenText,
|
type: "openText" as TSurveyElementTypeEnum.OpenText,
|
||||||
inputType: "text" as const,
|
inputType: "text" as const,
|
||||||
headline: { default: "$[projectName] Question" },
|
headline: { default: "$[workspaceName] Question" },
|
||||||
subheader: { default: "" },
|
subheader: { default: "" },
|
||||||
required: false,
|
required: false,
|
||||||
placeholder: { default: "" },
|
placeholder: { default: "" },
|
||||||
@@ -70,19 +70,19 @@ describe("replacePresetPlaceholders", () => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("replaces projectName placeholder in template name", () => {
|
test("replaces workspaceName placeholder in template name", () => {
|
||||||
const result = replacePresetPlaceholders(mockTemplate, mockProject);
|
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace);
|
||||||
expect(result.name).toBe("Test Project Survey");
|
expect(result.name).toBe("Test Workspace Survey");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("replaces projectName placeholder in element headline", () => {
|
test("replaces workspaceName placeholder in element headline", () => {
|
||||||
const result = replacePresetPlaceholders(mockTemplate, mockProject);
|
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace);
|
||||||
expect(result.blocks[0].elements[0].headline.default).toBe("Test Project Question");
|
expect(result.blocks[0].elements[0].headline.default).toBe("Test Workspace Question");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns a new object without mutating the original template", () => {
|
test("returns a new object without mutating the original template", () => {
|
||||||
const originalTemplate = structuredClone(mockTemplate);
|
const originalTemplate = structuredClone(mockTemplate);
|
||||||
const result = replacePresetPlaceholders(mockTemplate, mockProject);
|
const result = replacePresetPlaceholders(mockTemplate, mockWorkspace);
|
||||||
expect(result).not.toBe(mockTemplate);
|
expect(result).not.toBe(mockTemplate);
|
||||||
expect(mockTemplate).toEqual(originalTemplate);
|
expect(mockTemplate).toEqual(originalTemplate);
|
||||||
});
|
});
|
||||||
+5
-5
@@ -1,16 +1,16 @@
|
|||||||
import { TProject } from "@formbricks/types/project";
|
|
||||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||||
import { TXMTemplate } from "@formbricks/types/templates";
|
import { TXMTemplate } from "@formbricks/types/templates";
|
||||||
|
import { TWorkspace } from "@formbricks/types/workspace";
|
||||||
import { replaceElementPresetPlaceholders } from "@/lib/utils/templates";
|
import { replaceElementPresetPlaceholders } from "@/lib/utils/templates";
|
||||||
|
|
||||||
// replace all occurences of projectName with the actual project name in the current template
|
// replace all occurences of workspaceName with the actual workspace name in the current template
|
||||||
export const replacePresetPlaceholders = (template: TXMTemplate, project: TProject): TXMTemplate => {
|
export const replacePresetPlaceholders = (template: TXMTemplate, workspace: TWorkspace): TXMTemplate => {
|
||||||
const survey = structuredClone(template);
|
const survey = structuredClone(template);
|
||||||
|
|
||||||
const modifiedBlocks = survey.blocks.map((block: TSurveyBlock) => ({
|
const modifiedBlocks = survey.blocks.map((block: TSurveyBlock) => ({
|
||||||
...block,
|
...block,
|
||||||
elements: block.elements.map((element) => replaceElementPresetPlaceholders(element, project)),
|
elements: block.elements.map((element) => replaceElementPresetPlaceholders(element, workspace)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { ...survey, name: survey.name.replace("$[projectName]", project.name), blocks: modifiedBlocks };
|
return { ...survey, name: survey.name.replace("$[workspaceName]", workspace.name), blocks: modifiedBlocks };
|
||||||
};
|
};
|
||||||
+13
-20
@@ -2,11 +2,9 @@ import { XIcon } from "lucide-react";
|
|||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
import { XMTemplateList } from "@/app/(app)/(onboarding)/workspaces/[workspaceId]/xm-templates/components/XMTemplateList";
|
||||||
import { getEnvironment } from "@/lib/environment/service";
|
|
||||||
import { getProjectByEnvironmentId, getUserProjects } from "@/lib/project/service";
|
|
||||||
import { getUser } from "@/lib/user/service";
|
import { getUser } from "@/lib/user/service";
|
||||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
import { getUserWorkspaces, getWorkspace } from "@/lib/workspace/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -14,15 +12,15 @@ import { Header } from "@/modules/ui/components/header";
|
|||||||
|
|
||||||
interface XMTemplatePageProps {
|
interface XMTemplatePageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
environmentId: string;
|
workspaceId: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Page = async (props: XMTemplatePageProps) => {
|
const Page = async (props: XMTemplatePageProps) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
const environment = await getEnvironment(params.environmentId);
|
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new AuthenticationError(t("common.not_authenticated"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
@@ -31,29 +29,24 @@ const Page = async (props: XMTemplatePageProps) => {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new AuthenticationError(t("common.not_authenticated"));
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
}
|
}
|
||||||
if (!environment) {
|
|
||||||
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
|
const workspace = await getWorkspace(params.workspaceId);
|
||||||
|
if (!workspace) {
|
||||||
|
throw new ResourceNotFoundError(t("common.workspace"), params.workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
|
const workspaces = await getUserWorkspaces(session.user.id, workspace.organizationId);
|
||||||
|
|
||||||
const project = await getProjectByEnvironmentId(environment.id);
|
|
||||||
if (!project) {
|
|
||||||
throw new ResourceNotFoundError(t("common.workspace"), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const projects = await getUserProjects(session.user.id, organizationId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||||
<Header title={t("environments.xm-templates.headline")} />
|
<Header title={t("workspace.xm-templates.headline")} />
|
||||||
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
<XMTemplateList workspace={workspace} user={user} workspaceId={params.workspaceId} />
|
||||||
{projects.length >= 2 && (
|
{workspaces.length >= 2 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}/surveys`}>
|
<Link href={`/workspaces/${params.workspaceId}/surveys`}>
|
||||||
<XIcon className="h-7 w-7" strokeWidth={1.5} />
|
<XIcon className="h-7 w-7" strokeWidth={1.5} />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { redirect } from "next/navigation";
|
|
||||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
|
||||||
import { getEnvironment } from "@/lib/environment/service";
|
|
||||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
|
||||||
|
|
||||||
const SurveyEditorEnvironmentLayout = async (props: {
|
|
||||||
params: Promise<{ environmentId: string }>;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) => {
|
|
||||||
const params = await props.params;
|
|
||||||
|
|
||||||
const { children } = props;
|
|
||||||
|
|
||||||
const { t, session, user } = await environmentIdLayoutChecks(params.environmentId);
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return redirect(`/auth/login`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new AuthenticationError(t("common.not_authenticated"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const environment = await getEnvironment(params.environmentId);
|
|
||||||
|
|
||||||
if (!environment) {
|
|
||||||
throw new ResourceNotFoundError(t("common.environment"), params.environmentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen flex-col">
|
|
||||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SurveyEditorEnvironmentLayout;
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
|
import { getWorkspace } from "@/lib/workspace/service";
|
||||||
|
import { workspaceIdLayoutChecks } from "@/modules/workspaces/lib/utils";
|
||||||
|
|
||||||
|
const SurveyEditorWorkspaceLayout = async (props: {
|
||||||
|
params: Promise<{ workspaceId: string }>;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const params = await props.params;
|
||||||
|
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
const { t, session, user } = await workspaceIdLayoutChecks(params.workspaceId);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return redirect(`/auth/login`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new AuthenticationError(t("common.not_authenticated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = await getWorkspace(params.workspaceId);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new ResourceNotFoundError(t("common.workspace"), params.workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen flex-col">
|
||||||
|
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SurveyEditorWorkspaceLayout;
|
||||||
@@ -6,12 +6,12 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Confetti } from "@/modules/ui/components/confetti";
|
import { Confetti } from "@/modules/ui/components/confetti";
|
||||||
|
|
||||||
const BILLING_CONFIRMATION_ENVIRONMENT_ID_KEY = "billingConfirmationEnvironmentId";
|
const BILLING_CONFIRMATION_WORKSPACE_ID_KEY = "billingConfirmationWorkspaceId";
|
||||||
|
|
||||||
export const ConfirmationPage = () => {
|
export const ConfirmationPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showConfetti, setShowConfetti] = useState(false);
|
const [showConfetti, setShowConfetti] = useState(false);
|
||||||
const [resolvedEnvironmentId, setResolvedEnvironmentId] = useState<string | null>(null);
|
const [resolvedWorkspaceId, setResolvedWorkspaceId] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowConfetti(true);
|
setShowConfetti(true);
|
||||||
@@ -20,11 +20,9 @@ export const ConfirmationPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedEnvironmentId = globalThis.window.sessionStorage.getItem(
|
const storedWorkspaceId = globalThis.window.sessionStorage.getItem(BILLING_CONFIRMATION_WORKSPACE_ID_KEY);
|
||||||
BILLING_CONFIRMATION_ENVIRONMENT_ID_KEY
|
if (storedWorkspaceId) {
|
||||||
);
|
setResolvedWorkspaceId(storedWorkspaceId);
|
||||||
if (storedEnvironmentId) {
|
|
||||||
setResolvedEnvironmentId(storedEnvironmentId);
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -41,12 +39,7 @@ export const ConfirmationPage = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild className="w-full justify-center">
|
<Button asChild className="w-full justify-center">
|
||||||
<Link
|
<Link href={resolvedWorkspaceId ? `/workspaces/${resolvedWorkspaceId}/settings/billing` : "/"}>
|
||||||
href={
|
|
||||||
resolvedEnvironmentId
|
|
||||||
? `/environments/${resolvedEnvironmentId}/settings/billing`
|
|
||||||
: "/environments"
|
|
||||||
}>
|
|
||||||
{t("billing_confirmation.back_to_billing_overview")}
|
{t("billing_confirmation.back_to_billing_overview")}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
import { SettingsCard } from "@/app/(app)/workspaces/[workspaceId]/settings/components/SettingsCard";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
export const LoadingCard = ({
|
export const LoadingCard = ({
|
||||||
|
|||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
|
||||||
|
|
||||||
interface EnvironmentStorageHandlerProps {
|
|
||||||
environmentId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EnvironmentStorageHandler = ({ environmentId }: EnvironmentStorageHandlerProps) => {
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem(FORMBRICKS_ENVIRONMENT_ID_LS, environmentId);
|
|
||||||
}, [environmentId]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EnvironmentStorageHandler;
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
|
||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { Label } from "@/modules/ui/components/label";
|
|
||||||
import { Switch } from "@/modules/ui/components/switch";
|
|
||||||
|
|
||||||
interface EnvironmentSwitchProps {
|
|
||||||
environment: TEnvironment;
|
|
||||||
environments: TEnvironment[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EnvironmentSwitch = ({ environment, environments }: EnvironmentSwitchProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const router = useRouter();
|
|
||||||
const [isEnvSwitchChecked, setIsEnvSwitchChecked] = useState(environment?.type === "development");
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleEnvironmentChange = (environmentType: "production" | "development") => {
|
|
||||||
const newEnvironmentId = environments.find((e) => e.type === environmentType)?.id;
|
|
||||||
if (newEnvironmentId) {
|
|
||||||
router.push(`/environments/${newEnvironmentId}/`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleEnvSwitch = () => {
|
|
||||||
const newEnvironmentType = isEnvSwitchChecked ? "production" : "development";
|
|
||||||
setIsLoading(true);
|
|
||||||
setIsEnvSwitchChecked(!isEnvSwitchChecked);
|
|
||||||
handleEnvironmentChange(newEnvironmentType);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center space-x-2 rounded-lg p-2",
|
|
||||||
isEnvSwitchChecked ? "bg-slate-100 text-orange-800" : "hover:bg-slate-100"
|
|
||||||
)}>
|
|
||||||
<Label
|
|
||||||
htmlFor="development-mode"
|
|
||||||
className={cn("hover:cursor-pointer", isEnvSwitchChecked && "text-orange-800")}>
|
|
||||||
{t("common.dev_env")}
|
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
className="focus:ring-orange-800 data-[state=checked]:bg-orange-800"
|
|
||||||
id="development-mode"
|
|
||||||
disabled={isLoading}
|
|
||||||
checked={isEnvSwitchChecked}
|
|
||||||
onCheckedChange={toggleEnvSwitch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { ChevronDownIcon, CircleHelpIcon, Code2Icon, Loader2 } from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { BreadcrumbItem } from "@/modules/ui/components/breadcrumb";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/modules/ui/components/dropdown-menu";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
|
||||||
|
|
||||||
export const EnvironmentBreadcrumb = ({
|
|
||||||
environments,
|
|
||||||
currentEnvironment,
|
|
||||||
}: {
|
|
||||||
environments: { id: string; type: string }[];
|
|
||||||
currentEnvironment: { id: string; type: string };
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isEnvironmentDropdownOpen, setIsEnvironmentDropdownOpen] = useState(false);
|
|
||||||
const router = useRouter();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const handleEnvironmentChange = (environmentId: string) => {
|
|
||||||
if (environmentId === currentEnvironment.id) return;
|
|
||||||
setIsLoading(true);
|
|
||||||
router.push(`/environments/${environmentId}/`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const developmentTooltip = () => {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip delayDuration={0}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<CircleHelpIcon className="h-3 w-3" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="mt-2 border-none bg-red-800 text-white">
|
|
||||||
{t("common.development_environment_banner")}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BreadcrumbItem
|
|
||||||
isActive={isEnvironmentDropdownOpen}
|
|
||||||
isHighlighted={currentEnvironment.type === "development"}>
|
|
||||||
<DropdownMenu onOpenChange={setIsEnvironmentDropdownOpen}>
|
|
||||||
<DropdownMenuTrigger
|
|
||||||
className="flex cursor-pointer items-center gap-1 outline-none"
|
|
||||||
id="environmentDropdownTrigger"
|
|
||||||
asChild>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Code2Icon className="h-3 w-3" strokeWidth={1.5} />
|
|
||||||
<span className="capitalize">{currentEnvironment.type}</span>
|
|
||||||
{isLoading && <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1.5} />}
|
|
||||||
{currentEnvironment.type === "development" && developmentTooltip()}
|
|
||||||
{isEnvironmentDropdownOpen && <ChevronDownIcon className="h-3 w-3" strokeWidth={1.5} />}
|
|
||||||
</div>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="mt-2" align="start">
|
|
||||||
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
|
||||||
<Code2Icon className="mr-2 inline h-4 w-4" />
|
|
||||||
{t("common.choose_environment")}
|
|
||||||
</div>
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
{environments.map((env) => (
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
key={env.id}
|
|
||||||
checked={env.type === currentEnvironment.type}
|
|
||||||
onClick={() => handleEnvironmentChange(env.id)}
|
|
||||||
className="cursor-pointer">
|
|
||||||
<div className="flex items-center gap-2 capitalize">
|
|
||||||
<span>{env.type}</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { EnvironmentBreadcrumb } from "@/app/(app)/environments/[environmentId]/components/environment-breadcrumb";
|
|
||||||
import { OrganizationBreadcrumb } from "@/app/(app)/environments/[environmentId]/components/organization-breadcrumb";
|
|
||||||
import { ProjectBreadcrumb } from "@/app/(app)/environments/[environmentId]/components/project-breadcrumb";
|
|
||||||
import { Breadcrumb, BreadcrumbList } from "@/modules/ui/components/breadcrumb";
|
|
||||||
|
|
||||||
interface ProjectAndOrgSwitchProps {
|
|
||||||
currentOrganizationId: string;
|
|
||||||
currentOrganizationName?: string; // Optional: for pages without context
|
|
||||||
currentProjectId?: string;
|
|
||||||
currentProjectName?: string; // Optional: for pages without context
|
|
||||||
currentEnvironmentId?: string;
|
|
||||||
environments: { id: string; type: string }[];
|
|
||||||
isMultiOrgEnabled: boolean;
|
|
||||||
organizationProjectsLimit: number;
|
|
||||||
isFormbricksCloud: boolean;
|
|
||||||
isLicenseActive: boolean;
|
|
||||||
isOwnerOrManager: boolean;
|
|
||||||
isMember: boolean;
|
|
||||||
isBilling: boolean;
|
|
||||||
isMembershipPending: boolean;
|
|
||||||
isAccessControlAllowed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProjectAndOrgSwitch = ({
|
|
||||||
currentOrganizationId,
|
|
||||||
currentOrganizationName,
|
|
||||||
currentProjectId,
|
|
||||||
currentProjectName,
|
|
||||||
currentEnvironmentId,
|
|
||||||
environments,
|
|
||||||
isMultiOrgEnabled,
|
|
||||||
organizationProjectsLimit,
|
|
||||||
isFormbricksCloud,
|
|
||||||
isLicenseActive,
|
|
||||||
isOwnerOrManager,
|
|
||||||
isAccessControlAllowed,
|
|
||||||
isMember,
|
|
||||||
isBilling,
|
|
||||||
isMembershipPending,
|
|
||||||
}: ProjectAndOrgSwitchProps) => {
|
|
||||||
const currentEnvironment = environments.find((env) => env.id === currentEnvironmentId);
|
|
||||||
const showEnvironmentBreadcrumb = currentEnvironment?.type === "development";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Breadcrumb>
|
|
||||||
<BreadcrumbList className="gap-0">
|
|
||||||
<OrganizationBreadcrumb
|
|
||||||
currentOrganizationId={currentOrganizationId}
|
|
||||||
currentOrganizationName={currentOrganizationName}
|
|
||||||
currentEnvironmentId={currentEnvironmentId}
|
|
||||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
|
||||||
isFormbricksCloud={isFormbricksCloud}
|
|
||||||
isMember={isMember}
|
|
||||||
isOwnerOrManager={isOwnerOrManager}
|
|
||||||
isMembershipPending={isMembershipPending}
|
|
||||||
/>
|
|
||||||
{currentProjectId && currentEnvironmentId && (
|
|
||||||
<ProjectBreadcrumb
|
|
||||||
currentProjectId={currentProjectId}
|
|
||||||
currentProjectName={currentProjectName}
|
|
||||||
currentOrganizationId={currentOrganizationId}
|
|
||||||
currentEnvironmentId={currentEnvironmentId}
|
|
||||||
isOwnerOrManager={isOwnerOrManager}
|
|
||||||
organizationProjectsLimit={organizationProjectsLimit}
|
|
||||||
isFormbricksCloud={isFormbricksCloud}
|
|
||||||
isLicenseActive={isLicenseActive}
|
|
||||||
isAccessControlAllowed={isAccessControlAllowed}
|
|
||||||
isEnvironmentBreadcrumbVisible={showEnvironmentBreadcrumb}
|
|
||||||
isBilling={isBilling}
|
|
||||||
isMembershipPending={isMembershipPending}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showEnvironmentBreadcrumb && (
|
|
||||||
<EnvironmentBreadcrumb environments={environments} currentEnvironment={currentEnvironment} />
|
|
||||||
)}
|
|
||||||
</BreadcrumbList>
|
|
||||||
</Breadcrumb>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { createContext, useContext, useMemo } from "react";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
|
||||||
import { TProject } from "@formbricks/types/project";
|
|
||||||
|
|
||||||
export interface EnvironmentContextType {
|
|
||||||
environment: TEnvironment;
|
|
||||||
project: TProject;
|
|
||||||
organization: TOrganization;
|
|
||||||
organizationId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EnvironmentContext = createContext<EnvironmentContextType | null>(null);
|
|
||||||
|
|
||||||
export const useEnvironment = () => {
|
|
||||||
const context = useContext(EnvironmentContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useEnvironment must be used within an EnvironmentProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useProject = () => {
|
|
||||||
const context = useContext(EnvironmentContext);
|
|
||||||
if (!context) {
|
|
||||||
return { project: null };
|
|
||||||
}
|
|
||||||
return { project: context.project };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useOrganization = () => {
|
|
||||||
const context = useContext(EnvironmentContext);
|
|
||||||
if (!context) {
|
|
||||||
return { organization: null };
|
|
||||||
}
|
|
||||||
return { organization: context.organization };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Client wrapper component to be used in server components
|
|
||||||
interface EnvironmentContextWrapperProps {
|
|
||||||
environment: TEnvironment;
|
|
||||||
project: TProject;
|
|
||||||
organization: TOrganization;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EnvironmentContextWrapper = ({
|
|
||||||
environment,
|
|
||||||
project,
|
|
||||||
organization,
|
|
||||||
children,
|
|
||||||
}: EnvironmentContextWrapperProps) => {
|
|
||||||
const environmentContextValue = useMemo(
|
|
||||||
() => ({
|
|
||||||
environment,
|
|
||||||
project,
|
|
||||||
organization,
|
|
||||||
organizationId: project.organizationId,
|
|
||||||
}),
|
|
||||||
[environment, project, organization]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnvironmentContext.Provider value={environmentContextValue}>{children}</EnvironmentContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { getServerSession } from "next-auth";
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout";
|
|
||||||
import { EnvironmentContextWrapper } from "@/app/(app)/environments/[environmentId]/context/environment-context";
|
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
|
||||||
import { getEnvironmentLayoutData } from "@/modules/environments/lib/utils";
|
|
||||||
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
|
|
||||||
|
|
||||||
const EnvLayout = async (props: {
|
|
||||||
params: Promise<{ environmentId: string }>;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) => {
|
|
||||||
const params = await props.params;
|
|
||||||
const { children } = props;
|
|
||||||
|
|
||||||
// Check session first (required for userId)
|
|
||||||
const session = await getServerSession(authOptions);
|
|
||||||
if (!session?.user) {
|
|
||||||
return redirect(`/auth/login`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single consolidated data fetch (replaces ~12 individual fetches)
|
|
||||||
const layoutData = await getEnvironmentLayoutData(params.environmentId, session.user.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EnvironmentStorageHandler environmentId={params.environmentId} />
|
|
||||||
<EnvironmentContextWrapper
|
|
||||||
environment={layoutData.environment}
|
|
||||||
project={layoutData.project}
|
|
||||||
organization={layoutData.organization}>
|
|
||||||
<EnvironmentLayout layoutData={layoutData}>{children}</EnvironmentLayout>
|
|
||||||
</EnvironmentContextWrapper>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EnvLayout;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|
||||||
const params = await props.params;
|
|
||||||
return redirect(`/environments/${params.environmentId}/settings/profile`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
const Page = async (props: { params: Promise<{ environmentId: string; surveyId: string }> }) => {
|
|
||||||
const params = await props.params;
|
|
||||||
return redirect(`/environments/${params.environmentId}/surveys/${params.surveyId}/summary`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
import { AppConnectionLoading } from "@/modules/projects/settings/(setup)/app-connection/loading";
|
|
||||||
|
|
||||||
export default AppConnectionLoading;
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
import { AppConnectionPage } from "@/modules/projects/settings/(setup)/app-connection/page";
|
|
||||||
|
|
||||||
export default AppConnectionPage;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { GeneralSettingsLoading } from "@/modules/projects/settings/general/loading";
|
|
||||||
|
|
||||||
export default GeneralSettingsLoading;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { GeneralSettingsPage } from "@/modules/projects/settings/general/page";
|
|
||||||
|
|
||||||
export default GeneralSettingsPage;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { LanguagesLoading } from "@/modules/projects/settings/languages/loading";
|
|
||||||
|
|
||||||
export default LanguagesLoading;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { LanguagesPage } from "@/modules/projects/settings/languages/page";
|
|
||||||
|
|
||||||
export default LanguagesPage;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { ProjectSettingsLayout, metadata } from "@/modules/projects/settings/layout";
|
|
||||||
|
|
||||||
export { metadata };
|
|
||||||
export default ProjectSettingsLayout;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { ProjectLookSettingsLoading } from "@/modules/projects/settings/look/loading";
|
|
||||||
|
|
||||||
export default ProjectLookSettingsLoading;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { ProjectLookSettingsPage } from "@/modules/projects/settings/look/page";
|
|
||||||
|
|
||||||
export default ProjectLookSettingsPage;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { ProjectSettingsPage } from "@/modules/projects/settings/page";
|
|
||||||
|
|
||||||
export default ProjectSettingsPage;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { TagsLoading } from "@/modules/projects/settings/tags/loading";
|
|
||||||
|
|
||||||
export default TagsLoading;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { TagsPage } from "@/modules/projects/settings/tags/page";
|
|
||||||
|
|
||||||
export default TagsPage;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { ProjectTeams } from "@/modules/ee/teams/project-teams/page";
|
|
||||||
|
|
||||||
export default ProjectTeams;
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { ChartsListPage } from "@/modules/ee/analysis/charts/components/charts-list-page";
|
||||||
|
|
||||||
|
const ChartsPage = async (props: Readonly<{ params: Promise<{ workspaceId: string }> }>) => {
|
||||||
|
const { workspaceId } = await props.params;
|
||||||
|
return <ChartsListPage workspaceId={workspaceId} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChartsPage;
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import { DashboardDetailPage } from "@/modules/ee/analysis/dashboards/pages/dashboard-detail-page";
|
||||||
|
|
||||||
|
const Page = (props: { params: Promise<{ workspaceId: string; dashboardId: string }> }) => {
|
||||||
|
return <DashboardDetailPage params={props.params} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { DashboardsListPage } from "@/modules/ee/analysis/dashboards/pages/dashboards-list-page";
|
||||||
|
|
||||||
|
const DashboardsPage = async (props: Readonly<{ params: Promise<{ workspaceId: string }> }>) => {
|
||||||
|
const { workspaceId } = await props.params;
|
||||||
|
return <DashboardsListPage workspaceId={workspaceId} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardsPage;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { AnalysisListLoading } from "@/modules/ee/analysis/loading";
|
||||||
|
|
||||||
|
export default AnalysisListLoading;
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
import { AppConnectionLoading } from "@/modules/workspaces/settings/(setup)/app-connection/loading";
|
||||||
|
|
||||||
|
export default AppConnectionLoading;
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
import { AppConnectionPage } from "@/modules/workspaces/settings/(setup)/app-connection/page";
|
||||||
|
|
||||||
|
export default AppConnectionPage;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { WorkspaceSourcesPage as default } from "@/modules/workspaces/settings/sources/page";
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { GeneralSettingsLoading } from "@/modules/workspaces/settings/general/loading";
|
||||||
|
|
||||||
|
export default GeneralSettingsLoading;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { GeneralSettingsPage } from "@/modules/workspaces/settings/general/page";
|
||||||
|
|
||||||
|
export default GeneralSettingsPage;
|
||||||
+9
-10
@@ -8,15 +8,14 @@ import { capturePostHogEvent } from "@/lib/posthog";
|
|||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||||
import {
|
import {
|
||||||
getOrganizationIdFromEnvironmentId,
|
|
||||||
getOrganizationIdFromIntegrationId,
|
getOrganizationIdFromIntegrationId,
|
||||||
getProjectIdFromEnvironmentId,
|
getOrganizationIdFromWorkspaceId,
|
||||||
getProjectIdFromIntegrationId,
|
getWorkspaceIdFromIntegrationId,
|
||||||
} from "@/lib/utils/helper";
|
} from "@/lib/utils/helper";
|
||||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||||
|
|
||||||
const ZCreateOrUpdateIntegrationAction = z.object({
|
const ZCreateOrUpdateIntegrationAction = z.object({
|
||||||
environmentId: ZId,
|
workspaceId: ZId,
|
||||||
integrationData: ZIntegrationInput,
|
integrationData: ZIntegrationInput,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ export const createOrUpdateIntegrationAction = authenticatedActionClient
|
|||||||
.inputSchema(ZCreateOrUpdateIntegrationAction)
|
.inputSchema(ZCreateOrUpdateIntegrationAction)
|
||||||
.action(
|
.action(
|
||||||
withAuditLogging("createdUpdated", "integration", async ({ ctx, parsedInput }) => {
|
withAuditLogging("createdUpdated", "integration", async ({ ctx, parsedInput }) => {
|
||||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
const organizationId = await getOrganizationIdFromWorkspaceId(parsedInput.workspaceId);
|
||||||
|
|
||||||
await checkAuthorizationUpdated({
|
await checkAuthorizationUpdated({
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
@@ -35,15 +34,15 @@ export const createOrUpdateIntegrationAction = authenticatedActionClient
|
|||||||
roles: ["owner", "manager"],
|
roles: ["owner", "manager"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "projectTeam",
|
type: "workspaceTeam",
|
||||||
minPermission: "readWrite",
|
minPermission: "readWrite",
|
||||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
workspaceId: parsedInput.workspaceId,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||||
const result = await createOrUpdateIntegration(parsedInput.environmentId, parsedInput.integrationData);
|
const result = await createOrUpdateIntegration(parsedInput.workspaceId, parsedInput.integrationData);
|
||||||
ctx.auditLoggingCtx.integrationId = result.id;
|
ctx.auditLoggingCtx.integrationId = result.id;
|
||||||
ctx.auditLoggingCtx.newObject = result;
|
ctx.auditLoggingCtx.newObject = result;
|
||||||
|
|
||||||
@@ -73,8 +72,8 @@ export const deleteIntegrationAction = authenticatedActionClient.inputSchema(ZDe
|
|||||||
roles: ["owner", "manager"],
|
roles: ["owner", "manager"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "projectTeam",
|
type: "workspaceTeam",
|
||||||
projectId: await getProjectIdFromIntegrationId(parsedInput.integrationId),
|
workspaceId: await getWorkspaceIdFromIntegrationId(parsedInput.integrationId),
|
||||||
minPermission: "readWrite",
|
minPermission: "readWrite",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
+22
-22
@@ -17,9 +17,9 @@ import {
|
|||||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
import { createOrUpdateIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/actions";
|
||||||
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/BaseSelectDropdown";
|
import { BaseSelectDropdown } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/components/BaseSelectDropdown";
|
||||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/lib/airtable";
|
import { fetchTables } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/lib/airtable";
|
||||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { recallToHeadline } from "@/lib/utils/recall";
|
import { recallToHeadline } from "@/lib/utils/recall";
|
||||||
@@ -93,7 +93,7 @@ type EditModeProps =
|
|||||||
type AddIntegrationModalProps = {
|
type AddIntegrationModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpenWithStates: (v: boolean) => void;
|
setOpenWithStates: (v: boolean) => void;
|
||||||
environmentId: string;
|
workspaceId: string;
|
||||||
airtableArray: TIntegrationItem[];
|
airtableArray: TIntegrationItem[];
|
||||||
surveys: TSurvey[];
|
surveys: TSurvey[];
|
||||||
airtableIntegration: TIntegrationAirtable;
|
airtableIntegration: TIntegrationAirtable;
|
||||||
@@ -103,8 +103,8 @@ const NoBaseFoundError = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertTitle>{t("environments.integrations.airtable.no_bases_found")}</AlertTitle>
|
<AlertTitle>{t("workspace.integrations.airtable.no_bases_found")}</AlertTitle>
|
||||||
<AlertDescription>{t("environments.integrations.airtable.please_create_a_base")}</AlertDescription>
|
<AlertDescription>{t("workspace.integrations.airtable.please_create_a_base")}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -172,7 +172,7 @@ const renderElementSelection = ({
|
|||||||
export const AddIntegrationModal = ({
|
export const AddIntegrationModal = ({
|
||||||
open,
|
open,
|
||||||
setOpenWithStates,
|
setOpenWithStates,
|
||||||
environmentId,
|
workspaceId,
|
||||||
airtableArray,
|
airtableArray,
|
||||||
surveys,
|
surveys,
|
||||||
airtableIntegration,
|
airtableIntegration,
|
||||||
@@ -227,19 +227,19 @@ export const AddIntegrationModal = ({
|
|||||||
const submitHandler = async (data: IntegrationModalInputs) => {
|
const submitHandler = async (data: IntegrationModalInputs) => {
|
||||||
try {
|
try {
|
||||||
if (!data.base || data.base === "") {
|
if (!data.base || data.base === "") {
|
||||||
throw new Error(t("environments.integrations.airtable.please_select_a_base"));
|
throw new Error(t("workspace.integrations.airtable.please_select_a_base"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.table || data.table === "") {
|
if (!data.table || data.table === "") {
|
||||||
throw new Error(t("environments.integrations.airtable.please_select_a_table"));
|
throw new Error(t("workspace.integrations.airtable.please_select_a_table"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedSurvey) {
|
if (!selectedSurvey) {
|
||||||
throw new Error(t("environments.integrations.please_select_a_survey_error"));
|
throw new Error(t("workspace.integrations.please_select_a_survey_error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.elements.length === 0) {
|
if (data.elements.length === 0) {
|
||||||
throw new Error(t("environments.integrations.select_at_least_one_question_error"));
|
throw new Error(t("workspace.integrations.select_at_least_one_question_error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTable = tables.find((item) => item.id === data.table);
|
const currentTable = tables.find((item) => item.id === data.table);
|
||||||
@@ -270,7 +270,7 @@ export const AddIntegrationModal = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await createOrUpdateIntegrationAction({
|
const result = await createOrUpdateIntegrationAction({
|
||||||
environmentId,
|
workspaceId,
|
||||||
integrationData: airtableIntegrationData,
|
integrationData: airtableIntegrationData,
|
||||||
});
|
});
|
||||||
if (result?.serverError) {
|
if (result?.serverError) {
|
||||||
@@ -278,9 +278,9 @@ export const AddIntegrationModal = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
toast.success(t("environments.integrations.integration_updated_successfully"));
|
toast.success(t("workspace.integrations.integration_updated_successfully"));
|
||||||
} else {
|
} else {
|
||||||
toast.success(t("environments.integrations.integration_added_successfully"));
|
toast.success(t("workspace.integrations.integration_added_successfully"));
|
||||||
}
|
}
|
||||||
handleClose();
|
handleClose();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -289,7 +289,7 @@ export const AddIntegrationModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTable = async (baseId: string) => {
|
const handleTable = async (baseId: string) => {
|
||||||
const data = await fetchTables(environmentId, baseId);
|
const data = await fetchTables(workspaceId, baseId);
|
||||||
|
|
||||||
if (data.tables) {
|
if (data.tables) {
|
||||||
setTables(data.tables);
|
setTables(data.tables);
|
||||||
@@ -312,7 +312,7 @@ export const AddIntegrationModal = ({
|
|||||||
const integrationData = structuredClone(airtableIntegrationData);
|
const integrationData = structuredClone(airtableIntegrationData);
|
||||||
integrationData.config.data.splice(index, 1);
|
integrationData.config.data.splice(index, 1);
|
||||||
|
|
||||||
const result = await createOrUpdateIntegrationAction({ environmentId, integrationData });
|
const result = await createOrUpdateIntegrationAction({ workspaceId, integrationData });
|
||||||
if (result?.serverError) {
|
if (result?.serverError) {
|
||||||
toast.error(getFormattedErrorMessage(result));
|
toast.error(getFormattedErrorMessage(result));
|
||||||
return;
|
return;
|
||||||
@@ -320,7 +320,7 @@ export const AddIntegrationModal = ({
|
|||||||
handleClose();
|
handleClose();
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
toast.success(t("environments.integrations.integration_removed_successfully"));
|
toast.success(t("workspace.integrations.integration_removed_successfully"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : "Unknown error occurred");
|
toast.error(e instanceof Error ? e.message : "Unknown error occurred");
|
||||||
}
|
}
|
||||||
@@ -336,13 +336,13 @@ export const AddIntegrationModal = ({
|
|||||||
fill
|
fill
|
||||||
className="object-contain object-center"
|
className="object-contain object-center"
|
||||||
src={AirtableLogo}
|
src={AirtableLogo}
|
||||||
alt={t("environments.integrations.airtable.airtable_logo")}
|
alt={t("workspace.integrations.airtable.airtable_logo")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<DialogTitle>{t("environments.integrations.airtable.link_airtable_table")}</DialogTitle>
|
<DialogTitle>{t("workspace.integrations.airtable.link_airtable_table")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("environments.integrations.airtable.sync_responses_with_airtable")}
|
{t("workspace.integrations.airtable.sync_responses_with_airtable")}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -364,7 +364,7 @@ export const AddIntegrationModal = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<Label htmlFor="table">{t("environments.integrations.airtable.table_name")}</Label>
|
<Label htmlFor="table">{t("workspace.integrations.airtable.table_name")}</Label>
|
||||||
<div className="mt-1 flex">
|
<div className="mt-1 flex">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -427,7 +427,7 @@ export const AddIntegrationModal = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="m-1 text-xs text-slate-500">
|
<p className="m-1 text-xs text-slate-500">
|
||||||
{t("environments.integrations.create_survey_warning")}
|
{t("workspace.integrations.create_survey_warning")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
+6
-6
@@ -5,13 +5,13 @@ import { TIntegrationItem } from "@formbricks/types/integration";
|
|||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/ManageIntegration";
|
import { ManageIntegration } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/components/ManageIntegration";
|
||||||
import { authorize } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/lib/airtable";
|
import { authorize } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/lib/airtable";
|
||||||
import airtableLogo from "@/images/airtableLogo.svg";
|
import airtableLogo from "@/images/airtableLogo.svg";
|
||||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||||
|
|
||||||
interface AirtableWrapperProps {
|
interface AirtableWrapperProps {
|
||||||
environmentId: string;
|
workspaceId: string;
|
||||||
airtableArray: TIntegrationItem[];
|
airtableArray: TIntegrationItem[];
|
||||||
airtableIntegration?: TIntegrationAirtable;
|
airtableIntegration?: TIntegrationAirtable;
|
||||||
surveys: TSurvey[];
|
surveys: TSurvey[];
|
||||||
@@ -21,7 +21,7 @@ interface AirtableWrapperProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AirtableWrapper = ({
|
export const AirtableWrapper = ({
|
||||||
environmentId,
|
workspaceId,
|
||||||
airtableArray,
|
airtableArray,
|
||||||
airtableIntegration,
|
airtableIntegration,
|
||||||
surveys,
|
surveys,
|
||||||
@@ -34,7 +34,7 @@ export const AirtableWrapper = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleAirtableAuthorization = async () => {
|
const handleAirtableAuthorization = async () => {
|
||||||
authorize(environmentId, webAppUrl).then((url: string) => {
|
authorize(workspaceId, webAppUrl).then((url: string) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
window.location.replace(url);
|
window.location.replace(url);
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ export const AirtableWrapper = ({
|
|||||||
return isConnected && airtableIntegration ? (
|
return isConnected && airtableIntegration ? (
|
||||||
<ManageIntegration
|
<ManageIntegration
|
||||||
airtableArray={airtableArray}
|
airtableArray={airtableArray}
|
||||||
environmentId={environmentId}
|
workspaceId={workspaceId}
|
||||||
airtableIntegration={airtableIntegration}
|
airtableIntegration={airtableIntegration}
|
||||||
setIsConnected={setIsConnected}
|
setIsConnected={setIsConnected}
|
||||||
surveys={surveys}
|
surveys={surveys}
|
||||||
+1
-1
@@ -33,7 +33,7 @@ export const BaseSelectDropdown = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<Label htmlFor="base">{t("environments.integrations.airtable.airtable_base")}</Label>
|
<Label htmlFor="base">{t("workspace.integrations.airtable.airtable_base")}</Label>
|
||||||
<div className="mt-1 flex">
|
<div className="mt-1 flex">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
+13
-13
@@ -8,8 +8,8 @@ import { TIntegrationItem } from "@formbricks/types/integration";
|
|||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/actions";
|
||||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/AddIntegrationModal";
|
import { AddIntegrationModal } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/components/AddIntegrationModal";
|
||||||
import { timeSince } from "@/lib/time";
|
import { timeSince } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -19,7 +19,7 @@ import { IntegrationModalInputs } from "../lib/types";
|
|||||||
|
|
||||||
interface ManageIntegrationProps {
|
interface ManageIntegrationProps {
|
||||||
airtableIntegration: TIntegrationAirtable;
|
airtableIntegration: TIntegrationAirtable;
|
||||||
environmentId: string;
|
workspaceId: string;
|
||||||
setIsConnected: (data: boolean) => void;
|
setIsConnected: (data: boolean) => void;
|
||||||
surveys: TSurvey[];
|
surveys: TSurvey[];
|
||||||
airtableArray: TIntegrationItem[];
|
airtableArray: TIntegrationItem[];
|
||||||
@@ -27,12 +27,12 @@ interface ManageIntegrationProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ManageIntegration = (props: ManageIntegrationProps) => {
|
export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||||
const { airtableIntegration, environmentId, setIsConnected, surveys, airtableArray } = props;
|
const { airtableIntegration, workspaceId, setIsConnected, surveys, airtableArray } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const tableHeaders = [
|
const tableHeaders = [
|
||||||
t("common.survey"),
|
t("common.survey"),
|
||||||
t("environments.integrations.airtable.table_name"),
|
t("workspace.integrations.airtable.table_name"),
|
||||||
t("common.questions"),
|
t("common.questions"),
|
||||||
t("common.updated_at"),
|
t("common.updated_at"),
|
||||||
];
|
];
|
||||||
@@ -53,7 +53,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (deleteIntegrationActionResult?.data) {
|
if (deleteIntegrationActionResult?.data) {
|
||||||
toast.success(t("environments.integrations.integration_removed_successfully"));
|
toast.success(t("workspace.integrations.integration_removed_successfully"));
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = getFormattedErrorMessage(deleteIntegrationActionResult);
|
const errorMessage = getFormattedErrorMessage(deleteIntegrationActionResult);
|
||||||
@@ -77,7 +77,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
|
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
|
||||||
<span className="cursor-pointer text-slate-500">
|
<span className="cursor-pointer text-slate-500">
|
||||||
{t("environments.integrations.connected_with_email", {
|
{t("workspace.integrations.connected_with_email", {
|
||||||
email: airtableIntegration.config.email,
|
email: airtableIntegration.config.email,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
@@ -87,7 +87,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
|||||||
setDefaultValues(null);
|
setDefaultValues(null);
|
||||||
handleModal(true);
|
handleModal(true);
|
||||||
}}>
|
}}>
|
||||||
{t("environments.integrations.airtable.link_new_table")}
|
{t("workspace.integrations.airtable.link_new_table")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -130,21 +130,21 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4 w-full">
|
<div className="mt-4 w-full">
|
||||||
<EmptyState text={t("environments.integrations.airtable.no_integrations_yet")} />
|
<EmptyState text={t("workspace.integrations.airtable.no_integrations_yet")} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button variant="ghost" onClick={() => setIsDeleteIntegrationModalOpen(true)} className="mt-4">
|
<Button variant="ghost" onClick={() => setIsDeleteIntegrationModalOpen(true)} className="mt-4">
|
||||||
<Trash2Icon />
|
<Trash2Icon />
|
||||||
{t("environments.integrations.delete_integration")}
|
{t("workspace.integrations.delete_integration")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
open={isDeleteIntegrationModalOpen}
|
open={isDeleteIntegrationModalOpen}
|
||||||
setOpen={setIsDeleteIntegrationModalOpen}
|
setOpen={setIsDeleteIntegrationModalOpen}
|
||||||
deleteWhat={t("environments.integrations.airtable.airtable_integration")}
|
deleteWhat={t("workspace.integrations.airtable.airtable_integration")}
|
||||||
onDelete={handleDeleteIntegration}
|
onDelete={handleDeleteIntegration}
|
||||||
text={t("environments.integrations.delete_integration_confirmation")}
|
text={t("workspace.integrations.delete_integration_confirmation")}
|
||||||
isDeleting={isDeleting}
|
isDeleting={isDeleting}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
|||||||
airtableArray={airtableArray}
|
airtableArray={airtableArray}
|
||||||
open={isModalOpen}
|
open={isModalOpen}
|
||||||
setOpenWithStates={handleModal}
|
setOpenWithStates={handleModal}
|
||||||
environmentId={environmentId}
|
workspaceId={workspaceId}
|
||||||
surveys={surveys}
|
surveys={surveys}
|
||||||
airtableIntegration={airtableIntegration}
|
airtableIntegration={airtableIntegration}
|
||||||
{...data}
|
{...data}
|
||||||
+7
-7
@@ -13,7 +13,7 @@ vi.mock("@formbricks/logger", () => ({
|
|||||||
// Mock fetch
|
// Mock fetch
|
||||||
global.fetch = vi.fn();
|
global.fetch = vi.fn();
|
||||||
|
|
||||||
const environmentId = "test-env-id";
|
const workspaceId = "test-env-id";
|
||||||
const baseId = "test-base-id";
|
const baseId = "test-base-id";
|
||||||
const apiHost = "http://localhost:3000";
|
const apiHost = "http://localhost:3000";
|
||||||
|
|
||||||
@@ -36,11 +36,11 @@ describe("Airtable Library", () => {
|
|||||||
};
|
};
|
||||||
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
||||||
|
|
||||||
const tables = await fetchTables(environmentId, baseId);
|
const tables = await fetchTables(workspaceId, baseId);
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledWith(`/api/v1/integrations/airtable/tables?baseId=${baseId}`, {
|
expect(fetch).toHaveBeenCalledWith(`/api/v1/integrations/airtable/tables?baseId=${baseId}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { environmentId: environmentId },
|
headers: { workspaceId: workspaceId },
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
expect(tables).toEqual(mockTables);
|
expect(tables).toEqual(mockTables);
|
||||||
@@ -56,11 +56,11 @@ describe("Airtable Library", () => {
|
|||||||
};
|
};
|
||||||
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
||||||
|
|
||||||
const authUrl = await authorize(environmentId, apiHost);
|
const authUrl = await authorize(workspaceId, apiHost);
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledWith(`${apiHost}/api/v1/integrations/airtable`, {
|
expect(fetch).toHaveBeenCalledWith(`${apiHost}/api/v1/integrations/airtable`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { environmentId: environmentId },
|
headers: { workspaceId: workspaceId },
|
||||||
});
|
});
|
||||||
expect(authUrl).toBe(mockAuthUrl);
|
expect(authUrl).toBe(mockAuthUrl);
|
||||||
});
|
});
|
||||||
@@ -73,11 +73,11 @@ describe("Airtable Library", () => {
|
|||||||
};
|
};
|
||||||
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
vi.mocked(fetch).mockResolvedValue(mockResponse as Response);
|
||||||
|
|
||||||
await expect(authorize(environmentId, apiHost)).rejects.toThrow("Could not create response");
|
await expect(authorize(workspaceId, apiHost)).rejects.toThrow("Could not create response");
|
||||||
|
|
||||||
expect(fetch).toHaveBeenCalledWith(`${apiHost}/api/v1/integrations/airtable`, {
|
expect(fetch).toHaveBeenCalledWith(`${apiHost}/api/v1/integrations/airtable`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { environmentId: environmentId },
|
headers: { workspaceId: workspaceId },
|
||||||
});
|
});
|
||||||
expect(logger.error).toHaveBeenCalledWith({ errorText }, "authorize: Could not fetch airtable config");
|
expect(logger.error).toHaveBeenCalledWith({ errorText }, "authorize: Could not fetch airtable config");
|
||||||
});
|
});
|
||||||
+4
-4
@@ -1,20 +1,20 @@
|
|||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
import { TIntegrationAirtableTables } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtableTables } from "@formbricks/types/integration/airtable";
|
||||||
|
|
||||||
export const fetchTables = async (environmentId: string, baseId: string) => {
|
export const fetchTables = async (workspaceId: string, baseId: string) => {
|
||||||
const res = await fetch(`/api/v1/integrations/airtable/tables?baseId=${baseId}`, {
|
const res = await fetch(`/api/v1/integrations/airtable/tables?baseId=${baseId}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { environmentId: environmentId },
|
headers: { workspaceId },
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
const resJson = await res.json();
|
const resJson = await res.json();
|
||||||
return resJson.data as Promise<TIntegrationAirtableTables>;
|
return resJson.data as Promise<TIntegrationAirtableTables>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
|
export const authorize = async (workspaceId: string, apiHost: string): Promise<string> => {
|
||||||
const res = await fetch(`${apiHost}/api/v1/integrations/airtable`, {
|
const res = await fetch(`${apiHost}/api/v1/integrations/airtable`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { environmentId: environmentId },
|
headers: { workspaceId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
+11
-11
@@ -1,28 +1,28 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/AirtableWrapper";
|
import { AirtableWrapper } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/airtable/components/AirtableWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/lib/surveys";
|
||||||
import { getAirtableTables } from "@/lib/airtable/service";
|
import { getAirtableTables } from "@/lib/airtable/service";
|
||||||
import { AIRTABLE_CLIENT_ID, DEFAULT_LOCALE, WEBAPP_URL } from "@/lib/constants";
|
import { AIRTABLE_CLIENT_ID, DEFAULT_LOCALE, WEBAPP_URL } from "@/lib/constants";
|
||||||
import { getIntegrations } from "@/lib/integration/service";
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
import { getUserLocale } from "@/lib/user/service";
|
import { getUserLocale } from "@/lib/user/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
|
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
|
||||||
|
|
||||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const isEnabled = !!AIRTABLE_CLIENT_ID;
|
const isEnabled = !!AIRTABLE_CLIENT_ID;
|
||||||
|
|
||||||
const { isReadOnly, environment, session } = await getEnvironmentAuth(params.environmentId);
|
const { isReadOnly, session, workspace } = await getWorkspaceAuth(params.workspaceId);
|
||||||
|
|
||||||
const [surveys, integrations, locale] = await Promise.all([
|
const [surveys, integrations, locale] = await Promise.all([
|
||||||
getSurveys(params.environmentId),
|
getSurveys(workspace.id),
|
||||||
getIntegrations(params.environmentId),
|
getIntegrations(workspace.id),
|
||||||
getUserLocale(session.user.id),
|
getUserLocale(session.user.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
|
|
||||||
let airtableArray: TIntegrationItem[] = [];
|
let airtableArray: TIntegrationItem[] = [];
|
||||||
if (airtableIntegration?.config.key) {
|
if (airtableIntegration?.config.key) {
|
||||||
airtableArray = await getAirtableTables(params.environmentId);
|
airtableArray = await getAirtableTables(workspace.id);
|
||||||
}
|
}
|
||||||
if (isReadOnly) {
|
if (isReadOnly) {
|
||||||
return redirect("./");
|
return redirect("./");
|
||||||
@@ -40,14 +40,14 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/workspace/integrations`} />
|
<GoBackButton url={`${WEBAPP_URL}/workspaces/${params.workspaceId}/integrations`} />
|
||||||
<PageHeader pageTitle={t("environments.integrations.airtable.airtable_integration")} />
|
<PageHeader pageTitle={t("workspace.integrations.airtable.airtable_integration")} />
|
||||||
<div className="h-[75vh] w-full">
|
<div className="h-[75vh] w-full">
|
||||||
<AirtableWrapper
|
<AirtableWrapper
|
||||||
isEnabled={isEnabled}
|
isEnabled={isEnabled}
|
||||||
airtableIntegration={airtableIntegration}
|
airtableIntegration={airtableIntegration}
|
||||||
airtableArray={airtableArray}
|
airtableArray={airtableArray}
|
||||||
environmentId={environment.id}
|
workspaceId={workspace.id}
|
||||||
surveys={surveys}
|
surveys={surveys}
|
||||||
webAppUrl={WEBAPP_URL}
|
webAppUrl={WEBAPP_URL}
|
||||||
locale={locale ?? DEFAULT_LOCALE}
|
locale={locale ?? DEFAULT_LOCALE}
|
||||||
+10
-10
@@ -10,10 +10,10 @@ import { getSpreadsheetNameById, validateGoogleSheetsConnection } from "@/lib/go
|
|||||||
import { getIntegrationByType } from "@/lib/integration/service";
|
import { getIntegrationByType } from "@/lib/integration/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||||
|
|
||||||
const ZValidateGoogleSheetsConnectionAction = z.object({
|
const ZValidateGoogleSheetsConnectionAction = z.object({
|
||||||
environmentId: ZId,
|
workspaceId: ZId,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const validateGoogleSheetsConnectionAction = authenticatedActionClient
|
export const validateGoogleSheetsConnectionAction = authenticatedActionClient
|
||||||
@@ -21,21 +21,21 @@ export const validateGoogleSheetsConnectionAction = authenticatedActionClient
|
|||||||
.action(async ({ ctx, parsedInput }) => {
|
.action(async ({ ctx, parsedInput }) => {
|
||||||
await checkAuthorizationUpdated({
|
await checkAuthorizationUpdated({
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
organizationId: await getOrganizationIdFromWorkspaceId(parsedInput.workspaceId),
|
||||||
access: [
|
access: [
|
||||||
{
|
{
|
||||||
type: "organization",
|
type: "organization",
|
||||||
roles: ["owner", "manager"],
|
roles: ["owner", "manager"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "projectTeam",
|
type: "workspaceTeam",
|
||||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
workspaceId: parsedInput.workspaceId,
|
||||||
minPermission: "readWrite",
|
minPermission: "readWrite",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const integration = await getIntegrationByType(parsedInput.environmentId, "googleSheets");
|
const integration = await getIntegrationByType(parsedInput.workspaceId, "googleSheets");
|
||||||
if (!integration) {
|
if (!integration) {
|
||||||
return { data: false };
|
return { data: false };
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ export const validateGoogleSheetsConnectionAction = authenticatedActionClient
|
|||||||
|
|
||||||
const ZGetSpreadsheetNameByIdAction = z.object({
|
const ZGetSpreadsheetNameByIdAction = z.object({
|
||||||
googleSheetIntegration: ZIntegrationGoogleSheets,
|
googleSheetIntegration: ZIntegrationGoogleSheets,
|
||||||
environmentId: z.string(),
|
workspaceId: z.string(),
|
||||||
spreadsheetId: z.string(),
|
spreadsheetId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,15 +55,15 @@ export const getSpreadsheetNameByIdAction = authenticatedActionClient
|
|||||||
.action(async ({ ctx, parsedInput }) => {
|
.action(async ({ ctx, parsedInput }) => {
|
||||||
await checkAuthorizationUpdated({
|
await checkAuthorizationUpdated({
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
organizationId: await getOrganizationIdFromWorkspaceId(parsedInput.workspaceId),
|
||||||
access: [
|
access: [
|
||||||
{
|
{
|
||||||
type: "organization",
|
type: "organization",
|
||||||
roles: ["owner", "manager"],
|
roles: ["owner", "manager"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "projectTeam",
|
type: "workspaceTeam",
|
||||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
workspaceId: parsedInput.workspaceId,
|
||||||
minPermission: "readWrite",
|
minPermission: "readWrite",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
+22
-22
@@ -12,13 +12,13 @@ import {
|
|||||||
} from "@formbricks/types/integration/google-sheet";
|
} from "@formbricks/types/integration/google-sheet";
|
||||||
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
import { createOrUpdateIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/actions";
|
||||||
import { getSpreadsheetNameByIdAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/actions";
|
import { getSpreadsheetNameByIdAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/actions";
|
||||||
import {
|
import {
|
||||||
constructGoogleSheetsUrl,
|
constructGoogleSheetsUrl,
|
||||||
extractSpreadsheetIdFromUrl,
|
extractSpreadsheetIdFromUrl,
|
||||||
isValidGoogleSheetsUrl,
|
isValidGoogleSheetsUrl,
|
||||||
} from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/lib/util";
|
} from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/lib/util";
|
||||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||||
import {
|
import {
|
||||||
GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION,
|
GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION,
|
||||||
@@ -44,7 +44,7 @@ import { Input } from "@/modules/ui/components/input";
|
|||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
|
|
||||||
interface AddIntegrationModalProps {
|
interface AddIntegrationModalProps {
|
||||||
environmentId: string;
|
workspaceId: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
surveys: TSurvey[];
|
surveys: TSurvey[];
|
||||||
setOpen: (v: boolean) => void;
|
setOpen: (v: boolean) => void;
|
||||||
@@ -53,7 +53,7 @@ interface AddIntegrationModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AddIntegrationModal = ({
|
export const AddIntegrationModal = ({
|
||||||
environmentId,
|
workspaceId,
|
||||||
surveys,
|
surveys,
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
@@ -125,9 +125,9 @@ export const AddIntegrationModal = ({
|
|||||||
const showErrorMessageToast = (response: Awaited<ReturnType<typeof getSpreadsheetNameByIdAction>>) => {
|
const showErrorMessageToast = (response: Awaited<ReturnType<typeof getSpreadsheetNameByIdAction>>) => {
|
||||||
const errorMessage = getFormattedErrorMessage(response);
|
const errorMessage = getFormattedErrorMessage(response);
|
||||||
if (errorMessage === GOOGLE_SHEET_INTEGRATION_INVALID_GRANT) {
|
if (errorMessage === GOOGLE_SHEET_INTEGRATION_INVALID_GRANT) {
|
||||||
toast.error(t("environments.integrations.google_sheets.token_expired_error"));
|
toast.error(t("workspace.integrations.google_sheets.token_expired_error"));
|
||||||
} else if (errorMessage === GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION) {
|
} else if (errorMessage === GOOGLE_SHEET_INTEGRATION_INSUFFICIENT_PERMISSION) {
|
||||||
toast.error(t("environments.integrations.google_sheets.spreadsheet_permission_error"));
|
toast.error(t("workspace.integrations.google_sheets.spreadsheet_permission_error"));
|
||||||
} else {
|
} else {
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
}
|
}
|
||||||
@@ -136,19 +136,19 @@ export const AddIntegrationModal = ({
|
|||||||
const linkSheet = async () => {
|
const linkSheet = async () => {
|
||||||
try {
|
try {
|
||||||
if (!isValidGoogleSheetsUrl(spreadsheetUrl)) {
|
if (!isValidGoogleSheetsUrl(spreadsheetUrl)) {
|
||||||
throw new Error(t("environments.integrations.google_sheets.enter_a_valid_spreadsheet_url_error"));
|
throw new Error(t("workspace.integrations.google_sheets.enter_a_valid_spreadsheet_url_error"));
|
||||||
}
|
}
|
||||||
if (!selectedSurvey) {
|
if (!selectedSurvey) {
|
||||||
throw new Error(t("environments.integrations.please_select_a_survey_error"));
|
throw new Error(t("workspace.integrations.please_select_a_survey_error"));
|
||||||
}
|
}
|
||||||
if (selectedElements.length === 0) {
|
if (selectedElements.length === 0) {
|
||||||
throw new Error(t("environments.integrations.select_at_least_one_question_error"));
|
throw new Error(t("workspace.integrations.select_at_least_one_question_error"));
|
||||||
}
|
}
|
||||||
setIsLinkingSheet(true);
|
setIsLinkingSheet(true);
|
||||||
const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl);
|
const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl);
|
||||||
const spreadsheetNameResponse = await getSpreadsheetNameByIdAction({
|
const spreadsheetNameResponse = await getSpreadsheetNameByIdAction({
|
||||||
googleSheetIntegration,
|
googleSheetIntegration,
|
||||||
environmentId,
|
workspaceId,
|
||||||
spreadsheetId,
|
spreadsheetId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ export const AddIntegrationModal = ({
|
|||||||
googleSheetIntegrationData.config.data.push(integrationData);
|
googleSheetIntegrationData.config.data.push(integrationData);
|
||||||
}
|
}
|
||||||
const result = await createOrUpdateIntegrationAction({
|
const result = await createOrUpdateIntegrationAction({
|
||||||
environmentId,
|
workspaceId,
|
||||||
integrationData: googleSheetIntegrationData,
|
integrationData: googleSheetIntegrationData,
|
||||||
});
|
});
|
||||||
if (result?.serverError) {
|
if (result?.serverError) {
|
||||||
@@ -188,9 +188,9 @@ export const AddIntegrationModal = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selectedIntegration) {
|
if (selectedIntegration) {
|
||||||
toast.success(t("environments.integrations.integration_updated_successfully"));
|
toast.success(t("workspace.integrations.integration_updated_successfully"));
|
||||||
} else {
|
} else {
|
||||||
toast.success(t("environments.integrations.integration_added_successfully"));
|
toast.success(t("workspace.integrations.integration_added_successfully"));
|
||||||
}
|
}
|
||||||
resetForm();
|
resetForm();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -227,14 +227,14 @@ export const AddIntegrationModal = ({
|
|||||||
try {
|
try {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
const result = await createOrUpdateIntegrationAction({
|
const result = await createOrUpdateIntegrationAction({
|
||||||
environmentId,
|
workspaceId,
|
||||||
integrationData: googleSheetIntegrationData,
|
integrationData: googleSheetIntegrationData,
|
||||||
});
|
});
|
||||||
if (result?.serverError) {
|
if (result?.serverError) {
|
||||||
toast.error(getFormattedErrorMessage(result));
|
toast.error(getFormattedErrorMessage(result));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.success(t("environments.integrations.integration_removed_successfully"));
|
toast.success(t("workspace.integrations.integration_removed_successfully"));
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "Unknown error occurred");
|
toast.error(error instanceof Error ? error.message : "Unknown error occurred");
|
||||||
@@ -253,13 +253,13 @@ export const AddIntegrationModal = ({
|
|||||||
fill
|
fill
|
||||||
className="object-contain object-center"
|
className="object-contain object-center"
|
||||||
src={GoogleSheetLogo}
|
src={GoogleSheetLogo}
|
||||||
alt={t("environments.integrations.google_sheets.google_sheet_logo")}
|
alt={t("workspace.integrations.google_sheets.google_sheet_logo")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<DialogTitle>{t("environments.integrations.google_sheets.link_google_sheet")}</DialogTitle>
|
<DialogTitle>{t("workspace.integrations.google_sheets.link_google_sheet")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("environments.integrations.google_sheets.google_sheets_integration_description")}
|
{t("workspace.integrations.google_sheets.google_sheets_integration_description")}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -269,7 +269,7 @@ export const AddIntegrationModal = ({
|
|||||||
<div className="w-full space-y-4">
|
<div className="w-full space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<Label>{t("environments.integrations.google_sheets.spreadsheet_url")}</Label>
|
<Label>{t("workspace.integrations.google_sheets.spreadsheet_url")}</Label>
|
||||||
<Input
|
<Input
|
||||||
value={spreadsheetUrl}
|
value={spreadsheetUrl}
|
||||||
onChange={(e) => setSpreadsheetUrl(e.target.value)}
|
onChange={(e) => setSpreadsheetUrl(e.target.value)}
|
||||||
@@ -286,7 +286,7 @@ export const AddIntegrationModal = ({
|
|||||||
disabled={surveys.length === 0}
|
disabled={surveys.length === 0}
|
||||||
/>
|
/>
|
||||||
<p className="m-1 text-xs text-slate-500">
|
<p className="m-1 text-xs text-slate-500">
|
||||||
{surveys.length === 0 && t("environments.integrations.create_survey_warning")}
|
{surveys.length === 0 && t("workspace.integrations.create_survey_warning")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -361,7 +361,7 @@ export const AddIntegrationModal = ({
|
|||||||
<Button type="submit" loading={isLinkingSheet}>
|
<Button type="submit" loading={isLinkingSheet}>
|
||||||
{selectedIntegration
|
{selectedIntegration
|
||||||
? t("common.update")
|
? t("common.update")
|
||||||
: t("environments.integrations.google_sheets.link_google_sheet")}
|
: t("workspace.integrations.google_sheets.link_google_sheet")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
+9
-10
@@ -1,16 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
|
||||||
import {
|
import {
|
||||||
TIntegrationGoogleSheets,
|
TIntegrationGoogleSheets,
|
||||||
TIntegrationGoogleSheetsConfigData,
|
TIntegrationGoogleSheetsConfigData,
|
||||||
} from "@formbricks/types/integration/google-sheet";
|
} from "@formbricks/types/integration/google-sheet";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { validateGoogleSheetsConnectionAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/actions";
|
import { validateGoogleSheetsConnectionAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/actions";
|
||||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/ManageIntegration";
|
import { ManageIntegration } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/components/ManageIntegration";
|
||||||
import { authorize } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/lib/google";
|
import { authorize } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/lib/google";
|
||||||
import googleSheetLogo from "@/images/googleSheetsLogo.png";
|
import googleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||||
import { GOOGLE_SHEET_INTEGRATION_INVALID_GRANT } from "@/lib/googleSheet/constants";
|
import { GOOGLE_SHEET_INTEGRATION_INVALID_GRANT } from "@/lib/googleSheet/constants";
|
||||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||||
@@ -18,7 +17,7 @@ import { AddIntegrationModal } from "./AddIntegrationModal";
|
|||||||
|
|
||||||
interface GoogleSheetWrapperProps {
|
interface GoogleSheetWrapperProps {
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
environment: TEnvironment;
|
workspaceId: string;
|
||||||
surveys: TSurvey[];
|
surveys: TSurvey[];
|
||||||
googleSheetIntegration?: TIntegrationGoogleSheets;
|
googleSheetIntegration?: TIntegrationGoogleSheets;
|
||||||
webAppUrl: string;
|
webAppUrl: string;
|
||||||
@@ -27,7 +26,7 @@ interface GoogleSheetWrapperProps {
|
|||||||
|
|
||||||
export const GoogleSheetWrapper = ({
|
export const GoogleSheetWrapper = ({
|
||||||
isEnabled,
|
isEnabled,
|
||||||
environment,
|
workspaceId,
|
||||||
surveys,
|
surveys,
|
||||||
googleSheetIntegration,
|
googleSheetIntegration,
|
||||||
webAppUrl,
|
webAppUrl,
|
||||||
@@ -44,18 +43,18 @@ export const GoogleSheetWrapper = ({
|
|||||||
|
|
||||||
const validateConnection = useCallback(async () => {
|
const validateConnection = useCallback(async () => {
|
||||||
if (!isConnected || !googleSheetIntegration) return;
|
if (!isConnected || !googleSheetIntegration) return;
|
||||||
const response = await validateGoogleSheetsConnectionAction({ environmentId: environment.id });
|
const response = await validateGoogleSheetsConnectionAction({ workspaceId });
|
||||||
if (response?.serverError === GOOGLE_SHEET_INTEGRATION_INVALID_GRANT) {
|
if (response?.serverError === GOOGLE_SHEET_INTEGRATION_INVALID_GRANT) {
|
||||||
setShowReconnectButton(true);
|
setShowReconnectButton(true);
|
||||||
}
|
}
|
||||||
}, [environment.id, isConnected, googleSheetIntegration]);
|
}, [workspaceId, isConnected, googleSheetIntegration]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
validateConnection();
|
validateConnection();
|
||||||
}, [validateConnection]);
|
}, [validateConnection]);
|
||||||
|
|
||||||
const handleGoogleAuthorization = async () => {
|
const handleGoogleAuthorization = async () => {
|
||||||
authorize(environment.id, webAppUrl).then((url: string) => {
|
authorize(workspaceId, webAppUrl).then((url: string) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
window.location.replace(url);
|
window.location.replace(url);
|
||||||
}
|
}
|
||||||
@@ -67,7 +66,7 @@ export const GoogleSheetWrapper = ({
|
|||||||
{isConnected && googleSheetIntegration ? (
|
{isConnected && googleSheetIntegration ? (
|
||||||
<>
|
<>
|
||||||
<AddIntegrationModal
|
<AddIntegrationModal
|
||||||
environmentId={environment.id}
|
workspaceId={workspaceId}
|
||||||
surveys={surveys}
|
surveys={surveys}
|
||||||
open={isModalOpen}
|
open={isModalOpen}
|
||||||
setOpen={setIsModalOpen}
|
setOpen={setIsModalOpen}
|
||||||
+13
-13
@@ -9,7 +9,7 @@ import {
|
|||||||
TIntegrationGoogleSheetsConfigData,
|
TIntegrationGoogleSheetsConfigData,
|
||||||
} from "@formbricks/types/integration/google-sheet";
|
} from "@formbricks/types/integration/google-sheet";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/actions";
|
||||||
import { timeSince } from "@/lib/time";
|
import { timeSince } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Alert, AlertButton, AlertDescription } from "@/modules/ui/components/alert";
|
import { Alert, AlertButton, AlertDescription } from "@/modules/ui/components/alert";
|
||||||
@@ -53,7 +53,7 @@ export const ManageIntegration = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (deleteIntegrationActionResult?.data) {
|
if (deleteIntegrationActionResult?.data) {
|
||||||
toast.success(t("environments.integrations.integration_removed_successfully"));
|
toast.success(t("workspace.integrations.integration_removed_successfully"));
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = getFormattedErrorMessage(deleteIntegrationActionResult);
|
const errorMessage = getFormattedErrorMessage(deleteIntegrationActionResult);
|
||||||
@@ -77,10 +77,10 @@ export const ManageIntegration = ({
|
|||||||
{showReconnectButton && (
|
{showReconnectButton && (
|
||||||
<Alert variant="warning" size="small" className="mb-4 w-full">
|
<Alert variant="warning" size="small" className="mb-4 w-full">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{t("environments.integrations.google_sheets.reconnect_button_description")}
|
{t("workspace.integrations.google_sheets.reconnect_button_description")}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
<AlertButton onClick={handleGoogleAuthorization}>
|
<AlertButton onClick={handleGoogleAuthorization}>
|
||||||
{t("environments.integrations.google_sheets.reconnect_button")}
|
{t("workspace.integrations.google_sheets.reconnect_button")}
|
||||||
</AlertButton>
|
</AlertButton>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@@ -88,7 +88,7 @@ export const ManageIntegration = ({
|
|||||||
<div className="mr-6 flex items-center">
|
<div className="mr-6 flex items-center">
|
||||||
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
|
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
|
||||||
<span className="text-slate-500">
|
<span className="text-slate-500">
|
||||||
{t("environments.integrations.connected_with_email", {
|
{t("workspace.integrations.connected_with_email", {
|
||||||
email: googleSheetIntegration.config.email,
|
email: googleSheetIntegration.config.email,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
@@ -98,11 +98,11 @@ export const ManageIntegration = ({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="outline" onClick={handleGoogleAuthorization}>
|
<Button variant="outline" onClick={handleGoogleAuthorization}>
|
||||||
<RefreshCcwIcon className="mr-2 h-4 w-4" />
|
<RefreshCcwIcon className="mr-2 h-4 w-4" />
|
||||||
{t("environments.integrations.google_sheets.reconnect_button")}
|
{t("workspace.integrations.google_sheets.reconnect_button")}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{t("environments.integrations.google_sheets.reconnect_button_tooltip")}
|
{t("workspace.integrations.google_sheets.reconnect_button_tooltip")}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -111,12 +111,12 @@ export const ManageIntegration = ({
|
|||||||
setSelectedIntegration(null);
|
setSelectedIntegration(null);
|
||||||
setOpenAddIntegrationModal(true);
|
setOpenAddIntegrationModal(true);
|
||||||
}}>
|
}}>
|
||||||
{t("environments.integrations.google_sheets.link_new_sheet")}
|
{t("workspace.integrations.google_sheets.link_new_sheet")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{!integrationArray || integrationArray.length === 0 ? (
|
{!integrationArray || integrationArray.length === 0 ? (
|
||||||
<div className="mt-4 w-full">
|
<div className="mt-4 w-full">
|
||||||
<EmptyState text={t("environments.integrations.google_sheets.no_integrations_yet")} />
|
<EmptyState text={t("workspace.integrations.google_sheets.no_integrations_yet")} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4 flex w-full flex-col items-center justify-center">
|
<div className="mt-4 flex w-full flex-col items-center justify-center">
|
||||||
@@ -124,7 +124,7 @@ export const ManageIntegration = ({
|
|||||||
<div className="grid h-12 grid-cols-8 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
<div className="grid h-12 grid-cols-8 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||||
<div className="col-span-2 hidden text-center sm:block">{t("common.survey")}</div>
|
<div className="col-span-2 hidden text-center sm:block">{t("common.survey")}</div>
|
||||||
<div className="col-span-2 hidden text-center sm:block">
|
<div className="col-span-2 hidden text-center sm:block">
|
||||||
{t("environments.integrations.google_sheets.google_sheet_name")}
|
{t("workspace.integrations.google_sheets.google_sheet_name")}
|
||||||
</div>
|
</div>
|
||||||
<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.questions")}</div>
|
||||||
<div className="col-span-2 hidden text-center sm:block">{t("common.updated_at")}</div>
|
<div className="col-span-2 hidden text-center sm:block">{t("common.updated_at")}</div>
|
||||||
@@ -149,15 +149,15 @@ export const ManageIntegration = ({
|
|||||||
)}
|
)}
|
||||||
<Button variant="ghost" onClick={() => setIsDeleteIntegrationModalOpen(true)} className="mt-4">
|
<Button variant="ghost" onClick={() => setIsDeleteIntegrationModalOpen(true)} className="mt-4">
|
||||||
<Trash2Icon />
|
<Trash2Icon />
|
||||||
{t("environments.integrations.delete_integration")}
|
{t("workspace.integrations.delete_integration")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
open={isDeleteIntegrationModalOpen}
|
open={isDeleteIntegrationModalOpen}
|
||||||
setOpen={setIsDeleteIntegrationModalOpen}
|
setOpen={setIsDeleteIntegrationModalOpen}
|
||||||
deleteWhat={t("environments.integrations.google_sheets.google_connection")}
|
deleteWhat={t("workspace.integrations.google_sheets.google_connection")}
|
||||||
onDelete={handleDeleteIntegration}
|
onDelete={handleDeleteIntegration}
|
||||||
text={t("environments.integrations.google_sheets.google_connection_deletion_description")}
|
text={t("workspace.integrations.google_sheets.google_connection_deletion_description")}
|
||||||
isDeleting={isDeleting}
|
isDeleting={isDeleting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
+4
-4
@@ -14,10 +14,10 @@ const mockFetch = vi.fn();
|
|||||||
vi.stubGlobal("fetch", mockFetch);
|
vi.stubGlobal("fetch", mockFetch);
|
||||||
|
|
||||||
describe("authorize", () => {
|
describe("authorize", () => {
|
||||||
const environmentId = "test-env-id";
|
const workspaceId = "test-env-id";
|
||||||
const apiHost = "http://test.com";
|
const apiHost = "http://test.com";
|
||||||
const expectedUrl = `${apiHost}/api/google-sheet`;
|
const expectedUrl = `${apiHost}/api/google-sheet`;
|
||||||
const expectedHeaders = { environmentId: environmentId };
|
const expectedHeaders = { workspaceId: workspaceId };
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -30,7 +30,7 @@ describe("authorize", () => {
|
|||||||
json: async () => ({ data: { authUrl: mockAuthUrl } }),
|
json: async () => ({ data: { authUrl: mockAuthUrl } }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const authUrl = await authorize(environmentId, apiHost);
|
const authUrl = await authorize(workspaceId, apiHost);
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -47,7 +47,7 @@ describe("authorize", () => {
|
|||||||
text: async () => errorText,
|
text: async () => errorText,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(authorize(environmentId, apiHost)).rejects.toThrow("Could not create response");
|
await expect(authorize(workspaceId, apiHost)).rejects.toThrow("Could not create response");
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
|
|
||||||
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
|
export const authorize = async (workspaceId: string, apiHost: string): Promise<string> => {
|
||||||
const res = await fetch(`${apiHost}/api/google-sheet`, {
|
const res = await fetch(`${apiHost}/api/google-sheet`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { environmentId: environmentId },
|
headers: { workspaceId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
+2
-2
@@ -11,7 +11,7 @@ const Loading = () => {
|
|||||||
<GoBackButton />
|
<GoBackButton />
|
||||||
<div className="mb-6 text-right">
|
<div className="mb-6 text-right">
|
||||||
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
|
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
|
||||||
{t("environments.integrations.google_sheets.link_new_sheet")}
|
{t("workspace.integrations.google_sheets.link_new_sheet")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ const Loading = () => {
|
|||||||
<div className="grid h-12 grid-cols-12 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
<div className="grid h-12 grid-cols-12 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||||
<div className="col-span-4 text-center">{t("common.survey")}</div>
|
<div className="col-span-4 text-center">{t("common.survey")}</div>
|
||||||
<div className="col-span-4 text-center">
|
<div className="col-span-4 text-center">
|
||||||
{t("environments.integrations.google_sheets.google_sheet_name")}
|
{t("workspace.integrations.google_sheets.google_sheet_name")}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 text-center">{t("common.questions")}</div>
|
<div className="col-span-2 text-center">{t("common.questions")}</div>
|
||||||
<div className="col-span-2 text-center">{t("common.updated_at")}</div>
|
<div className="col-span-2 text-center">{t("common.updated_at")}</div>
|
||||||
+10
-10
@@ -1,7 +1,7 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/GoogleSheetWrapper";
|
import { GoogleSheetWrapper } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/lib/surveys";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LOCALE,
|
DEFAULT_LOCALE,
|
||||||
GOOGLE_SHEETS_CLIENT_ID,
|
GOOGLE_SHEETS_CLIENT_ID,
|
||||||
@@ -12,21 +12,21 @@ import {
|
|||||||
import { getIntegrations } from "@/lib/integration/service";
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
import { getUserLocale } from "@/lib/user/service";
|
import { getUserLocale } from "@/lib/user/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
|
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
|
||||||
|
|
||||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
|
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
|
||||||
|
|
||||||
const { isReadOnly, environment, session } = await getEnvironmentAuth(params.environmentId);
|
const { isReadOnly, session, workspace } = await getWorkspaceAuth(params.workspaceId);
|
||||||
|
|
||||||
const [surveys, integrations, locale] = await Promise.all([
|
const [surveys, integrations, locale] = await Promise.all([
|
||||||
getSurveys(params.environmentId),
|
getSurveys(workspace.id),
|
||||||
getIntegrations(params.environmentId),
|
getIntegrations(workspace.id),
|
||||||
getUserLocale(session.user.id),
|
getUserLocale(session.user.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -39,12 +39,12 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/workspace/integrations`} />
|
<GoBackButton url={`${WEBAPP_URL}/workspaces/${params.workspaceId}/integrations`} />
|
||||||
<PageHeader pageTitle={t("environments.integrations.google_sheets.google_sheets_integration")} />
|
<PageHeader pageTitle={t("workspace.integrations.google_sheets.google_sheets_integration")} />
|
||||||
<div className="h-[75vh] w-full">
|
<div className="h-[75vh] w-full">
|
||||||
<GoogleSheetWrapper
|
<GoogleSheetWrapper
|
||||||
isEnabled={isEnabled}
|
isEnabled={isEnabled}
|
||||||
environment={environment}
|
workspaceId={workspace.id}
|
||||||
surveys={surveys}
|
surveys={surveys}
|
||||||
googleSheetIntegration={googleSheetIntegration}
|
googleSheetIntegration={googleSheetIntegration}
|
||||||
webAppUrl={WEBAPP_URL}
|
webAppUrl={WEBAPP_URL}
|
||||||
+8
-8
@@ -35,7 +35,7 @@ vi.mock("react", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const environmentId = "test-environment-id";
|
const workspaceId = "test-environment-id";
|
||||||
// Use 'as any' to bypass complex type matching for mock data
|
// Use 'as any' to bypass complex type matching for mock data
|
||||||
const mockPrismaSurveys = [
|
const mockPrismaSurveys = [
|
||||||
{ id: "survey1", name: "Survey 1", status: "inProgress", updatedAt: new Date() },
|
{ id: "survey1", name: "Survey 1", status: "inProgress", updatedAt: new Date() },
|
||||||
@@ -58,7 +58,7 @@ const mockTransformedSurveys: TSurvey[] = [
|
|||||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||||
hiddenFields: { enabled: false },
|
hiddenFields: { enabled: false },
|
||||||
type: "app", // Changed type to web to match original file
|
type: "app", // Changed type to web to match original file
|
||||||
environmentId: environmentId,
|
workspaceId: workspaceId,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
languages: [],
|
languages: [],
|
||||||
@@ -80,7 +80,7 @@ const mockTransformedSurveys: TSurvey[] = [
|
|||||||
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
|
||||||
hiddenFields: { enabled: false },
|
hiddenFields: { enabled: false },
|
||||||
type: "app",
|
type: "app",
|
||||||
environmentId: environmentId,
|
workspaceId: workspaceId,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
languages: [],
|
languages: [],
|
||||||
@@ -98,14 +98,14 @@ describe("getSurveys", () => {
|
|||||||
return { ...found } as TSurvey;
|
return { ...found } as TSurvey;
|
||||||
});
|
});
|
||||||
|
|
||||||
const surveys = await getSurveys(environmentId);
|
const surveys = await getSurveys(workspaceId);
|
||||||
|
|
||||||
expect(surveys).toEqual(mockTransformedSurveys);
|
expect(surveys).toEqual(mockTransformedSurveys);
|
||||||
// Use expect.any(ZId) for the Zod schema validation check
|
// Use expect.any(ZId) for the Zod schema validation check
|
||||||
expect(validateInputs).toHaveBeenCalledWith([environmentId, expect.any(Object)]); // Adjusted expectation
|
expect(validateInputs).toHaveBeenCalledWith([workspaceId, expect.any(Object)]); // Adjusted expectation
|
||||||
expect(prisma.survey.findMany).toHaveBeenCalledWith({
|
expect(prisma.survey.findMany).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
environmentId,
|
workspaceId,
|
||||||
status: {
|
status: {
|
||||||
not: "completed",
|
not: "completed",
|
||||||
},
|
},
|
||||||
@@ -129,7 +129,7 @@ describe("getSurveys", () => {
|
|||||||
|
|
||||||
vi.mocked(prisma.survey.findMany).mockRejectedValueOnce(prismaError);
|
vi.mocked(prisma.survey.findMany).mockRejectedValueOnce(prismaError);
|
||||||
|
|
||||||
await expect(getSurveys(environmentId)).rejects.toThrow(DatabaseError);
|
await expect(getSurveys(workspaceId)).rejects.toThrow(DatabaseError);
|
||||||
expect(logger.error).toHaveBeenCalledWith({ error: prismaError }, "getSurveys: Could not fetch surveys");
|
expect(logger.error).toHaveBeenCalledWith({ error: prismaError }, "getSurveys: Could not fetch surveys");
|
||||||
// React cache is already mocked globally - no need to check it here
|
// React cache is already mocked globally - no need to check it here
|
||||||
});
|
});
|
||||||
@@ -139,7 +139,7 @@ describe("getSurveys", () => {
|
|||||||
|
|
||||||
vi.mocked(prisma.survey.findMany).mockRejectedValueOnce(genericError);
|
vi.mocked(prisma.survey.findMany).mockRejectedValueOnce(genericError);
|
||||||
|
|
||||||
await expect(getSurveys(environmentId)).rejects.toThrow(genericError);
|
await expect(getSurveys(workspaceId)).rejects.toThrow(genericError);
|
||||||
expect(logger.error).not.toHaveBeenCalled();
|
expect(logger.error).not.toHaveBeenCalled();
|
||||||
// React cache is already mocked globally - no need to check it here
|
// React cache is already mocked globally - no need to check it here
|
||||||
});
|
});
|
||||||
+3
-3
@@ -10,13 +10,13 @@ import { selectSurvey } from "@/lib/survey/service";
|
|||||||
import { transformPrismaSurvey } from "@/lib/survey/utils";
|
import { transformPrismaSurvey } from "@/lib/survey/utils";
|
||||||
import { validateInputs } from "@/lib/utils/validate";
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
|
|
||||||
export const getSurveys = reactCache(async (environmentId: string): Promise<TSurvey[]> => {
|
export const getSurveys = reactCache(async (workspaceId: string): Promise<TSurvey[]> => {
|
||||||
validateInputs([environmentId, ZId]);
|
validateInputs([workspaceId, ZId]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const surveysPrisma = await prisma.survey.findMany({
|
const surveysPrisma = await prisma.survey.findMany({
|
||||||
where: {
|
where: {
|
||||||
environmentId,
|
workspaceId,
|
||||||
status: {
|
status: {
|
||||||
not: "completed",
|
not: "completed",
|
||||||
},
|
},
|
||||||
+9
-9
@@ -14,7 +14,7 @@ vi.mock("@formbricks/database", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const environmentId = "test-environment-id";
|
const workspaceId = "test-workspace-id";
|
||||||
const sourceZapier = "zapier";
|
const sourceZapier = "zapier";
|
||||||
|
|
||||||
describe("getWebhookCountBySource", () => {
|
describe("getWebhookCountBySource", () => {
|
||||||
@@ -26,16 +26,16 @@ describe("getWebhookCountBySource", () => {
|
|||||||
const mockCount = 5;
|
const mockCount = 5;
|
||||||
vi.mocked(prisma.webhook.count).mockResolvedValue(mockCount);
|
vi.mocked(prisma.webhook.count).mockResolvedValue(mockCount);
|
||||||
|
|
||||||
const count = await getWebhookCountBySource(environmentId, sourceZapier);
|
const count = await getWebhookCountBySource(workspaceId, sourceZapier);
|
||||||
|
|
||||||
expect(count).toBe(mockCount);
|
expect(count).toBe(mockCount);
|
||||||
expect(validateInputs).toHaveBeenCalledWith(
|
expect(validateInputs).toHaveBeenCalledWith(
|
||||||
[environmentId, expect.any(Object)],
|
[workspaceId, expect.any(Object)],
|
||||||
[sourceZapier, expect.any(Object)]
|
[sourceZapier, expect.any(Object)]
|
||||||
);
|
);
|
||||||
expect(prisma.webhook.count).toHaveBeenCalledWith({
|
expect(prisma.webhook.count).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
environmentId,
|
workspaceId: workspaceId,
|
||||||
source: sourceZapier,
|
source: sourceZapier,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -45,16 +45,16 @@ describe("getWebhookCountBySource", () => {
|
|||||||
const mockCount = 10;
|
const mockCount = 10;
|
||||||
vi.mocked(prisma.webhook.count).mockResolvedValue(mockCount);
|
vi.mocked(prisma.webhook.count).mockResolvedValue(mockCount);
|
||||||
|
|
||||||
const count = await getWebhookCountBySource(environmentId);
|
const count = await getWebhookCountBySource(workspaceId);
|
||||||
|
|
||||||
expect(count).toBe(mockCount);
|
expect(count).toBe(mockCount);
|
||||||
expect(validateInputs).toHaveBeenCalledWith(
|
expect(validateInputs).toHaveBeenCalledWith(
|
||||||
[environmentId, expect.any(Object)],
|
[workspaceId, expect.any(Object)],
|
||||||
[undefined, expect.any(Object)]
|
[undefined, expect.any(Object)]
|
||||||
);
|
);
|
||||||
expect(prisma.webhook.count).toHaveBeenCalledWith({
|
expect(prisma.webhook.count).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
environmentId,
|
workspaceId: workspaceId,
|
||||||
source: undefined,
|
source: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -67,7 +67,7 @@ describe("getWebhookCountBySource", () => {
|
|||||||
});
|
});
|
||||||
vi.mocked(prisma.webhook.count).mockRejectedValue(prismaError);
|
vi.mocked(prisma.webhook.count).mockRejectedValue(prismaError);
|
||||||
|
|
||||||
await expect(getWebhookCountBySource(environmentId, sourceZapier)).rejects.toThrow(DatabaseError);
|
await expect(getWebhookCountBySource(workspaceId, sourceZapier)).rejects.toThrow(DatabaseError);
|
||||||
expect(prisma.webhook.count).toHaveBeenCalledTimes(1);
|
expect(prisma.webhook.count).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ describe("getWebhookCountBySource", () => {
|
|||||||
const genericError = new Error("Something went wrong");
|
const genericError = new Error("Something went wrong");
|
||||||
vi.mocked(prisma.webhook.count).mockRejectedValue(genericError);
|
vi.mocked(prisma.webhook.count).mockRejectedValue(genericError);
|
||||||
|
|
||||||
await expect(getWebhookCountBySource(environmentId)).rejects.toThrow(genericError);
|
await expect(getWebhookCountBySource(workspaceId)).rejects.toThrow(genericError);
|
||||||
expect(prisma.webhook.count).toHaveBeenCalledTimes(1);
|
expect(prisma.webhook.count).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+3
-3
@@ -6,15 +6,15 @@ import { DatabaseError } from "@formbricks/types/errors";
|
|||||||
import { validateInputs } from "@/lib/utils/validate";
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
|
|
||||||
export const getWebhookCountBySource = async (
|
export const getWebhookCountBySource = async (
|
||||||
environmentId: string,
|
workspaceId: string,
|
||||||
source?: Webhook["source"]
|
source?: Webhook["source"]
|
||||||
): Promise<number> => {
|
): Promise<number> => {
|
||||||
validateInputs([environmentId, ZId], [source, z.string().optional()]);
|
validateInputs([workspaceId, ZId], [source, z.string().optional()]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const count = await prisma.webhook.count({
|
const count = await prisma.webhook.count({
|
||||||
where: {
|
where: {
|
||||||
environmentId,
|
workspaceId,
|
||||||
source,
|
source,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
+23
-23
@@ -15,12 +15,12 @@ import {
|
|||||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
import { createOrUpdateIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/actions";
|
||||||
import {
|
import {
|
||||||
MappingRow,
|
MappingRow,
|
||||||
TMapping,
|
TMapping,
|
||||||
createEmptyMapping,
|
createEmptyMapping,
|
||||||
} from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/MappingRow";
|
} from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/notion/components/MappingRow";
|
||||||
import NotionLogo from "@/images/notion.png";
|
import NotionLogo from "@/images/notion.png";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { recallToHeadline } from "@/lib/utils/recall";
|
import { recallToHeadline } from "@/lib/utils/recall";
|
||||||
@@ -39,7 +39,7 @@ import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
|||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
|
|
||||||
interface AddIntegrationModalProps {
|
interface AddIntegrationModalProps {
|
||||||
environmentId: string;
|
workspaceId: string;
|
||||||
surveys: TSurvey[];
|
surveys: TSurvey[];
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
@@ -49,7 +49,7 @@ interface AddIntegrationModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AddIntegrationModal = ({
|
export const AddIntegrationModal = ({
|
||||||
environmentId,
|
workspaceId,
|
||||||
surveys,
|
surveys,
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
@@ -175,18 +175,18 @@ export const AddIntegrationModal = ({
|
|||||||
const linkDatabase = async () => {
|
const linkDatabase = async () => {
|
||||||
try {
|
try {
|
||||||
if (!selectedDatabase) {
|
if (!selectedDatabase) {
|
||||||
throw new Error(t("environments.integrations.notion.please_select_a_database"));
|
throw new Error(t("workspace.integrations.notion.please_select_a_database"));
|
||||||
}
|
}
|
||||||
if (!selectedSurvey) {
|
if (!selectedSurvey) {
|
||||||
throw new Error(t("environments.integrations.please_select_a_survey_error"));
|
throw new Error(t("workspace.integrations.please_select_a_survey_error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.length === 1 && (!mapping[0].element.id || !mapping[0].column.id)) {
|
if (mapping.length === 1 && (!mapping[0].element.id || !mapping[0].column.id)) {
|
||||||
throw new Error(t("environments.integrations.notion.please_select_at_least_one_mapping"));
|
throw new Error(t("workspace.integrations.notion.please_select_at_least_one_mapping"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.filter((m) => m.error).length > 0) {
|
if (mapping.filter((m) => m.error).length > 0) {
|
||||||
throw new Error(t("environments.integrations.notion.please_resolve_mapping_errors"));
|
throw new Error(t("workspace.integrations.notion.please_resolve_mapping_errors"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -194,7 +194,7 @@ export const AddIntegrationModal = ({
|
|||||||
mapping.filter((m) => m.element.id && !m.column.id).length >= 1
|
mapping.filter((m) => m.element.id && !m.column.id).length >= 1
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
t("environments.integrations.notion.please_complete_mapping_fields_with_notion_property")
|
t("workspace.integrations.notion.please_complete_mapping_fields_with_notion_property")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ export const AddIntegrationModal = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await createOrUpdateIntegrationAction({
|
const result = await createOrUpdateIntegrationAction({
|
||||||
environmentId,
|
workspaceId,
|
||||||
integrationData: notionIntegrationData,
|
integrationData: notionIntegrationData,
|
||||||
});
|
});
|
||||||
if (result?.serverError) {
|
if (result?.serverError) {
|
||||||
@@ -227,9 +227,9 @@ export const AddIntegrationModal = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selectedIntegration) {
|
if (selectedIntegration) {
|
||||||
toast.success(t("environments.integrations.integration_updated_successfully"));
|
toast.success(t("workspace.integrations.integration_updated_successfully"));
|
||||||
} else {
|
} else {
|
||||||
toast.success(t("environments.integrations.integration_added_successfully"));
|
toast.success(t("workspace.integrations.integration_added_successfully"));
|
||||||
}
|
}
|
||||||
resetForm();
|
resetForm();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -245,14 +245,14 @@ export const AddIntegrationModal = ({
|
|||||||
try {
|
try {
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
const result = await createOrUpdateIntegrationAction({
|
const result = await createOrUpdateIntegrationAction({
|
||||||
environmentId,
|
workspaceId,
|
||||||
integrationData: notionIntegrationData,
|
integrationData: notionIntegrationData,
|
||||||
});
|
});
|
||||||
if (result?.serverError) {
|
if (result?.serverError) {
|
||||||
toast.error(getFormattedErrorMessage(result));
|
toast.error(getFormattedErrorMessage(result));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toast.success(t("environments.integrations.integration_removed_successfully"));
|
toast.success(t("workspace.integrations.integration_removed_successfully"));
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "Unknown error occurred");
|
toast.error(error instanceof Error ? error.message : "Unknown error occurred");
|
||||||
@@ -283,13 +283,13 @@ export const AddIntegrationModal = ({
|
|||||||
fill
|
fill
|
||||||
className="object-contain object-center"
|
className="object-contain object-center"
|
||||||
src={NotionLogo}
|
src={NotionLogo}
|
||||||
alt={t("environments.integrations.notion.notion_logo")}
|
alt={t("workspace.integrations.notion.notion_logo")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<DialogTitle>{t("environments.integrations.notion.link_notion_database")}</DialogTitle>
|
<DialogTitle>{t("workspace.integrations.notion.link_notion_database")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t("environments.integrations.notion.notion_integration_description")}
|
{t("workspace.integrations.notion.notion_integration_description")}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,7 +301,7 @@ export const AddIntegrationModal = ({
|
|||||||
<div>
|
<div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<DropdownSelector
|
<DropdownSelector
|
||||||
label={t("environments.integrations.notion.select_a_database")}
|
label={t("workspace.integrations.notion.select_a_database")}
|
||||||
items={databases.map((d) => ({
|
items={databases.map((d) => ({
|
||||||
id: d.id,
|
id: d.id,
|
||||||
name: (d as any).title?.[0]?.plain_text,
|
name: (d as any).title?.[0]?.plain_text,
|
||||||
@@ -314,13 +314,13 @@ export const AddIntegrationModal = ({
|
|||||||
{selectedDatabase && hasMatchingId && (
|
{selectedDatabase && hasMatchingId && (
|
||||||
<p className="text-xs text-amber-700">
|
<p className="text-xs text-amber-700">
|
||||||
<strong>{t("common.warning")}:</strong>{" "}
|
<strong>{t("common.warning")}:</strong>{" "}
|
||||||
{t("environments.integrations.notion.duplicate_connection_warning")}
|
{t("workspace.integrations.notion.duplicate_connection_warning")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="m-1 text-xs text-slate-500">
|
<p className="m-1 text-xs text-slate-500">
|
||||||
{databases.length === 0 &&
|
{databases.length === 0 &&
|
||||||
t(
|
t(
|
||||||
"environments.integrations.notion.create_at_least_one_database_to_setup_this_integration"
|
"workspace.integrations.notion.create_at_least_one_database_to_setup_this_integration"
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -333,13 +333,13 @@ export const AddIntegrationModal = ({
|
|||||||
disabled={surveys.length === 0}
|
disabled={surveys.length === 0}
|
||||||
/>
|
/>
|
||||||
<p className="m-1 text-xs text-slate-500">
|
<p className="m-1 text-xs text-slate-500">
|
||||||
{surveys.length === 0 && t("environments.integrations.create_survey_warning")}
|
{surveys.length === 0 && t("workspace.integrations.create_survey_warning")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{selectedDatabase && selectedSurvey && (
|
{selectedDatabase && selectedSurvey && (
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
{t("environments.integrations.notion.map_formbricks_fields_to_notion_property")}
|
{t("workspace.integrations.notion.map_formbricks_fields_to_notion_property")}
|
||||||
</Label>
|
</Label>
|
||||||
<div className="mt-1 space-y-2 overflow-y-auto">
|
<div className="mt-1 space-y-2 overflow-y-auto">
|
||||||
{mapping.map((m, idx) => (
|
{mapping.map((m, idx) => (
|
||||||
@@ -388,7 +388,7 @@ export const AddIntegrationModal = ({
|
|||||||
type="submit"
|
type="submit"
|
||||||
loading={isLinkingDatabase}
|
loading={isLinkingDatabase}
|
||||||
disabled={mapping.filter((m) => m.error).length > 0}>
|
disabled={mapping.filter((m) => m.error).length > 0}>
|
||||||
{selectedIntegration ? t("common.update") : t("environments.integrations.notion.link_database")}
|
{selectedIntegration ? t("common.update") : t("workspace.integrations.notion.link_database")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
+11
-11
@@ -6,7 +6,7 @@ import toast from "react-hot-toast";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
|
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/actions";
|
||||||
import { timeSince } from "@/lib/time";
|
import { timeSince } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -50,7 +50,7 @@ export const ManageIntegration = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (deleteIntegrationActionResult?.data) {
|
if (deleteIntegrationActionResult?.data) {
|
||||||
toast.success(t("environments.integrations.integration_removed_successfully"));
|
toast.success(t("workspace.integrations.integration_removed_successfully"));
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = getFormattedErrorMessage(deleteIntegrationActionResult);
|
const errorMessage = getFormattedErrorMessage(deleteIntegrationActionResult);
|
||||||
@@ -72,7 +72,7 @@ export const ManageIntegration = ({
|
|||||||
<div className="mr-6 flex items-center">
|
<div className="mr-6 flex items-center">
|
||||||
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
|
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
|
||||||
<span className="text-slate-500">
|
<span className="text-slate-500">
|
||||||
{t("environments.integrations.notion.connected_with_workspace", {
|
{t("workspace.integrations.notion.connected_with_workspace", {
|
||||||
workspace: notionIntegration.config.key.workspace_name,
|
workspace: notionIntegration.config.key.workspace_name,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
@@ -82,10 +82,10 @@ export const ManageIntegration = ({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="outline" onClick={handleNotionAuthorization}>
|
<Button variant="outline" onClick={handleNotionAuthorization}>
|
||||||
<RefreshCcwIcon className="mr-2 h-4 w-4" />
|
<RefreshCcwIcon className="mr-2 h-4 w-4" />
|
||||||
{t("environments.integrations.notion.update_connection")}
|
{t("workspace.integrations.notion.update_connection")}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{t("environments.integrations.notion.update_connection_tooltip")}</TooltipContent>
|
<TooltipContent>{t("workspace.integrations.notion.update_connection_tooltip")}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<Button
|
<Button
|
||||||
@@ -93,12 +93,12 @@ export const ManageIntegration = ({
|
|||||||
setSelectedIntegration(null);
|
setSelectedIntegration(null);
|
||||||
setOpenAddIntegrationModal(true);
|
setOpenAddIntegrationModal(true);
|
||||||
}}>
|
}}>
|
||||||
{t("environments.integrations.notion.link_new_database")}
|
{t("workspace.integrations.notion.link_new_database")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{!integrationArray || integrationArray.length === 0 ? (
|
{!integrationArray || integrationArray.length === 0 ? (
|
||||||
<div className="mt-4 w-full">
|
<div className="mt-4 w-full">
|
||||||
<EmptyState text={t("environments.integrations.notion.no_databases_found")} />
|
<EmptyState text={t("workspace.integrations.notion.no_databases_found")} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-4 flex w-full flex-col items-center justify-center">
|
<div className="mt-4 flex w-full flex-col items-center justify-center">
|
||||||
@@ -106,7 +106,7 @@ export const ManageIntegration = ({
|
|||||||
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||||
<div className="col-span-2 hidden text-center sm:block">{t("common.survey")}</div>
|
<div className="col-span-2 hidden text-center sm:block">{t("common.survey")}</div>
|
||||||
<div className="col-span-2 hidden text-center sm:block">
|
<div className="col-span-2 hidden text-center sm:block">
|
||||||
{t("environments.integrations.notion.database_name")}
|
{t("workspace.integrations.notion.database_name")}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 hidden text-center sm:block">{t("common.updated_at")}</div>
|
<div className="col-span-2 hidden text-center sm:block">{t("common.updated_at")}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,15 +129,15 @@ export const ManageIntegration = ({
|
|||||||
)}
|
)}
|
||||||
<Button variant="ghost" onClick={() => setIsDeleteIntegrationModalOpen(true)} className="mt-4">
|
<Button variant="ghost" onClick={() => setIsDeleteIntegrationModalOpen(true)} className="mt-4">
|
||||||
<Trash2Icon />
|
<Trash2Icon />
|
||||||
{t("environments.integrations.delete_integration")}
|
{t("workspace.integrations.delete_integration")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
open={isDeleteIntegrationModalOpen}
|
open={isDeleteIntegrationModalOpen}
|
||||||
setOpen={setIsDeleteIntegrationModalOpen}
|
setOpen={setIsDeleteIntegrationModalOpen}
|
||||||
deleteWhat={t("environments.integrations.notion.notion_integration")}
|
deleteWhat={t("workspace.integrations.notion.notion_integration")}
|
||||||
onDelete={handleDeleteIntegration}
|
onDelete={handleDeleteIntegration}
|
||||||
text={t("environments.integrations.delete_integration_confirmation")}
|
text={t("workspace.integrations.delete_integration_confirmation")}
|
||||||
isDeleting={isDeleting}
|
isDeleting={isDeleting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
+5
-5
@@ -8,7 +8,7 @@ import {
|
|||||||
ERRORS,
|
ERRORS,
|
||||||
TYPE_MAPPING,
|
TYPE_MAPPING,
|
||||||
UNSUPPORTED_TYPES_BY_NOTION,
|
UNSUPPORTED_TYPES_BY_NOTION,
|
||||||
} from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/constants";
|
} from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/notion/constants";
|
||||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||||
import { getElementTypes } from "@/modules/survey/lib/elements";
|
import { getElementTypes } from "@/modules/survey/lib/elements";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -48,7 +48,7 @@ const MappingErrorMessage = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
-{" "}
|
-{" "}
|
||||||
{t("environments.integrations.notion.col_name_of_type_is_not_supported", {
|
{t("workspace.integrations.notion.col_name_of_type_is_not_supported", {
|
||||||
col_name: col.name,
|
col_name: col.name,
|
||||||
type: col.type,
|
type: col.type,
|
||||||
})}
|
})}
|
||||||
@@ -60,7 +60,7 @@ const MappingErrorMessage = ({
|
|||||||
if (!element) return null;
|
if (!element) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{t("environments.integrations.notion.que_name_of_type_cant_be_mapped_to", {
|
{t("workspace.integrations.notion.que_name_of_type_cant_be_mapped_to", {
|
||||||
que_name: elem.name,
|
que_name: elem.name,
|
||||||
question_label: element.label,
|
question_label: element.label,
|
||||||
col_name: col.name,
|
col_name: col.name,
|
||||||
@@ -194,7 +194,7 @@ export const MappingRow = ({
|
|||||||
<div className="flex w-full items-center">
|
<div className="flex w-full items-center">
|
||||||
<div className="max-w-full flex-1">
|
<div className="max-w-full flex-1">
|
||||||
<DropdownSelector
|
<DropdownSelector
|
||||||
placeholder={t("environments.integrations.notion.select_a_survey_question")}
|
placeholder={t("workspace.integrations.notion.select_a_survey_question")}
|
||||||
items={filteredElementItems}
|
items={filteredElementItems}
|
||||||
selectedItem={mapping?.[idx]?.element}
|
selectedItem={mapping?.[idx]?.element}
|
||||||
setSelectedItem={handleElementSelect}
|
setSelectedItem={handleElementSelect}
|
||||||
@@ -204,7 +204,7 @@ export const MappingRow = ({
|
|||||||
<div className="h-px w-4 border-t border-t-slate-300" />
|
<div className="h-px w-4 border-t border-t-slate-300" />
|
||||||
<div className="max-w-full flex-1">
|
<div className="max-w-full flex-1">
|
||||||
<DropdownSelector
|
<DropdownSelector
|
||||||
placeholder={t("environments.integrations.notion.select_a_field_to_map")}
|
placeholder={t("workspace.integrations.notion.select_a_field_to_map")}
|
||||||
items={getFilteredDbItems()}
|
items={getFilteredDbItems()}
|
||||||
selectedItem={mapping?.[idx]?.column}
|
selectedItem={mapping?.[idx]?.column}
|
||||||
setSelectedItem={handleColumnSelect}
|
setSelectedItem={handleColumnSelect}
|
||||||
+6
-7
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
|
||||||
import {
|
import {
|
||||||
TIntegrationNotion,
|
TIntegrationNotion,
|
||||||
TIntegrationNotionConfigData,
|
TIntegrationNotionConfigData,
|
||||||
@@ -9,8 +8,8 @@ import {
|
|||||||
} from "@formbricks/types/integration/notion";
|
} from "@formbricks/types/integration/notion";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/AddIntegrationModal";
|
import { AddIntegrationModal } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/notion/components/AddIntegrationModal";
|
||||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/ManageIntegration";
|
import { ManageIntegration } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/notion/components/ManageIntegration";
|
||||||
import notionLogo from "@/images/notion.png";
|
import notionLogo from "@/images/notion.png";
|
||||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||||
import { authorize } from "../lib/notion";
|
import { authorize } from "../lib/notion";
|
||||||
@@ -18,7 +17,7 @@ import { authorize } from "../lib/notion";
|
|||||||
interface NotionWrapperProps {
|
interface NotionWrapperProps {
|
||||||
notionIntegration: TIntegrationNotion | undefined;
|
notionIntegration: TIntegrationNotion | undefined;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
environment: TEnvironment;
|
workspaceId: string;
|
||||||
webAppUrl: string;
|
webAppUrl: string;
|
||||||
surveys: TSurvey[];
|
surveys: TSurvey[];
|
||||||
databasesArray: TIntegrationNotionDatabase[];
|
databasesArray: TIntegrationNotionDatabase[];
|
||||||
@@ -28,7 +27,7 @@ interface NotionWrapperProps {
|
|||||||
export const NotionWrapper = ({
|
export const NotionWrapper = ({
|
||||||
notionIntegration,
|
notionIntegration,
|
||||||
enabled,
|
enabled,
|
||||||
environment,
|
workspaceId,
|
||||||
webAppUrl,
|
webAppUrl,
|
||||||
surveys,
|
surveys,
|
||||||
databasesArray,
|
databasesArray,
|
||||||
@@ -43,7 +42,7 @@ export const NotionWrapper = ({
|
|||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const handleNotionAuthorization = async () => {
|
const handleNotionAuthorization = async () => {
|
||||||
authorize(environment.id, webAppUrl).then((url: string) => {
|
authorize(workspaceId, webAppUrl).then((url: string) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
window.location.replace(url);
|
window.location.replace(url);
|
||||||
}
|
}
|
||||||
@@ -55,7 +54,7 @@ export const NotionWrapper = ({
|
|||||||
{isConnected && notionIntegration ? (
|
{isConnected && notionIntegration ? (
|
||||||
<>
|
<>
|
||||||
<AddIntegrationModal
|
<AddIntegrationModal
|
||||||
environmentId={environment.id}
|
workspaceId={workspaceId}
|
||||||
surveys={surveys}
|
surveys={surveys}
|
||||||
open={isModalOpen}
|
open={isModalOpen}
|
||||||
setOpen={setIsModalOpen}
|
setOpen={setIsModalOpen}
|
||||||
+4
-4
@@ -14,10 +14,10 @@ const mockFetch = vi.fn();
|
|||||||
vi.stubGlobal("fetch", mockFetch);
|
vi.stubGlobal("fetch", mockFetch);
|
||||||
|
|
||||||
describe("authorize", () => {
|
describe("authorize", () => {
|
||||||
const environmentId = "test-env-id";
|
const workspaceId = "test-env-id";
|
||||||
const apiHost = "http://test.com";
|
const apiHost = "http://test.com";
|
||||||
const expectedUrl = `${apiHost}/api/v1/integrations/notion`;
|
const expectedUrl = `${apiHost}/api/v1/integrations/notion`;
|
||||||
const expectedHeaders = { environmentId: environmentId };
|
const expectedHeaders = { workspaceId: workspaceId };
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -30,7 +30,7 @@ describe("authorize", () => {
|
|||||||
json: async () => ({ data: { authUrl: mockAuthUrl } }),
|
json: async () => ({ data: { authUrl: mockAuthUrl } }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const authUrl = await authorize(environmentId, apiHost);
|
const authUrl = await authorize(workspaceId, apiHost);
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -47,7 +47,7 @@ describe("authorize", () => {
|
|||||||
text: async () => errorText,
|
text: async () => errorText,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(authorize(environmentId, apiHost)).rejects.toThrow("Could not create response");
|
await expect(authorize(workspaceId, apiHost)).rejects.toThrow("Could not create response");
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
expect(mockFetch).toHaveBeenCalledWith(expectedUrl, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
|
|
||||||
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
|
export const authorize = async (workspaceId: string, apiHost: string): Promise<string> => {
|
||||||
const res = await fetch(`${apiHost}/api/v1/integrations/notion`, {
|
const res = await fetch(`${apiHost}/api/v1/integrations/notion`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { environmentId: environmentId },
|
headers: { workspaceId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
+2
-2
@@ -11,14 +11,14 @@ const Loading = () => {
|
|||||||
<GoBackButton />
|
<GoBackButton />
|
||||||
<div className="mb-6 text-right">
|
<div className="mb-6 text-right">
|
||||||
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
|
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
|
||||||
{t("environments.integrations.notion.link_database")}
|
{t("workspace.integrations.notion.link_database")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-slate-200">
|
<div className="rounded-lg border border-slate-200">
|
||||||
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||||
<div className="col-span-2 text-center">{t("common.survey")}</div>
|
<div className="col-span-2 text-center">{t("common.survey")}</div>
|
||||||
<div className="col-span-2 text-center">{t("environments.integrations.notion.database_name")}</div>
|
<div className="col-span-2 text-center">{t("workspace.integrations.notion.database_name")}</div>
|
||||||
<div className="col-span-2 text-center">{t("common.updated_at")}</div>
|
<div className="col-span-2 text-center">{t("common.updated_at")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-cols-7">
|
<div className="grid-cols-7">
|
||||||
+10
-10
@@ -1,7 +1,7 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/lib/surveys";
|
||||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/NotionWrapper";
|
import { NotionWrapper } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/notion/components/NotionWrapper";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LOCALE,
|
DEFAULT_LOCALE,
|
||||||
NOTION_AUTH_URL,
|
NOTION_AUTH_URL,
|
||||||
@@ -14,12 +14,12 @@ import { getIntegrationByType } from "@/lib/integration/service";
|
|||||||
import { getNotionDatabases } from "@/lib/notion/service";
|
import { getNotionDatabases } from "@/lib/notion/service";
|
||||||
import { getUserLocale } from "@/lib/user/service";
|
import { getUserLocale } from "@/lib/user/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
|
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
|
||||||
|
|
||||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const enabled = !!(
|
const enabled = !!(
|
||||||
@@ -29,17 +29,17 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
NOTION_REDIRECT_URI
|
NOTION_REDIRECT_URI
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isReadOnly, environment, session } = await getEnvironmentAuth(params.environmentId);
|
const { isReadOnly, session, workspace } = await getWorkspaceAuth(params.workspaceId);
|
||||||
|
|
||||||
const [surveys, notionIntegration, locale] = await Promise.all([
|
const [surveys, notionIntegration, locale] = await Promise.all([
|
||||||
getSurveys(params.environmentId),
|
getSurveys(workspace.id),
|
||||||
getIntegrationByType(params.environmentId, "notion"),
|
getIntegrationByType(workspace.id, "notion"),
|
||||||
getUserLocale(session.user.id),
|
getUserLocale(session.user.id),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let databasesArray: TIntegrationNotionDatabase[] = [];
|
let databasesArray: TIntegrationNotionDatabase[] = [];
|
||||||
if (notionIntegration && (notionIntegration as TIntegrationNotion).config.key?.bot_id) {
|
if (notionIntegration && (notionIntegration as TIntegrationNotion).config.key?.bot_id) {
|
||||||
databasesArray = (await getNotionDatabases(environment.id)) ?? [];
|
databasesArray = (await getNotionDatabases(workspace.id)) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isReadOnly) {
|
if (isReadOnly) {
|
||||||
@@ -49,11 +49,11 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
return (
|
return (
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<GoBackButton url={"./"} />
|
<GoBackButton url={"./"} />
|
||||||
<PageHeader pageTitle={t("environments.integrations.notion.notion_integration")} />
|
<PageHeader pageTitle={t("workspace.integrations.notion.notion_integration")} />
|
||||||
<NotionWrapper
|
<NotionWrapper
|
||||||
enabled={enabled}
|
enabled={enabled}
|
||||||
surveys={surveys}
|
surveys={surveys}
|
||||||
environment={environment}
|
workspaceId={workspace.id}
|
||||||
notionIntegration={notionIntegration as TIntegrationNotion}
|
notionIntegration={notionIntegration as TIntegrationNotion}
|
||||||
webAppUrl={WEBAPP_URL}
|
webAppUrl={WEBAPP_URL}
|
||||||
databasesArray={databasesArray}
|
databasesArray={databasesArray}
|
||||||
+31
-31
@@ -2,7 +2,7 @@ import { TFunction } from "i18next";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { TIntegrationType } from "@formbricks/types/integration";
|
import { TIntegrationType } from "@formbricks/types/integration";
|
||||||
import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/webhook";
|
import { getWebhookCountBySource } from "@/app/(app)/workspaces/[workspaceId]/(workspace)/integrations/lib/webhook";
|
||||||
import ActivePiecesLogo from "@/images/activepieces.webp";
|
import ActivePiecesLogo from "@/images/activepieces.webp";
|
||||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||||
import GoogleSheetsLogo from "@/images/googleSheetsLogo.png";
|
import GoogleSheetsLogo from "@/images/googleSheetsLogo.png";
|
||||||
@@ -17,11 +17,11 @@ import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
|||||||
import { getIntegrations } from "@/lib/integration/service";
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
import { getBillingFallbackPath } from "@/lib/membership/navigation";
|
import { getBillingFallbackPath } from "@/lib/membership/navigation";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
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 { Card } from "@/modules/ui/components/integration-card";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
|
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
|
||||||
|
import { WorkspaceConfigNavigation } from "@/modules/workspaces/settings/components/workspace-config-navigation";
|
||||||
|
|
||||||
const getStatusText = (count: number, t: TFunction, type: string) => {
|
const getStatusText = (count: number, t: TFunction, type: string) => {
|
||||||
if (count === 1) return `1 ${type}`;
|
if (count === 1) return `1 ${type}`;
|
||||||
@@ -29,11 +29,11 @@ const getStatusText = (count: number, t: TFunction, type: string) => {
|
|||||||
return `${count} ${type}s`;
|
return `${count} ${type}s`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
const Page = async (props: { params: Promise<{ workspaceId: string }> }) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|
||||||
const { isReadOnly, environment, isBilling } = await getEnvironmentAuth(params.environmentId);
|
const { isReadOnly, isBilling, workspace } = await getWorkspaceAuth(params.workspaceId);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
integrations,
|
integrations,
|
||||||
@@ -43,19 +43,19 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
n8nwebhookCount,
|
n8nwebhookCount,
|
||||||
activePiecesWebhookCount,
|
activePiecesWebhookCount,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getIntegrations(params.environmentId),
|
getIntegrations(workspace.id),
|
||||||
getWebhookCountBySource(params.environmentId, "user"),
|
getWebhookCountBySource(workspace.id, "user"),
|
||||||
getWebhookCountBySource(params.environmentId, "zapier"),
|
getWebhookCountBySource(workspace.id, "zapier"),
|
||||||
getWebhookCountBySource(params.environmentId, "make"),
|
getWebhookCountBySource(workspace.id, "make"),
|
||||||
getWebhookCountBySource(params.environmentId, "n8n"),
|
getWebhookCountBySource(workspace.id, "n8n"),
|
||||||
getWebhookCountBySource(params.environmentId, "activepieces"),
|
getWebhookCountBySource(workspace.id, "activepieces"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isIntegrationConnected = (type: TIntegrationType) =>
|
const isIntegrationConnected = (type: TIntegrationType) =>
|
||||||
integrations.some((integration) => integration.type === type);
|
integrations.some((integration) => integration.type === type);
|
||||||
|
|
||||||
if (isBilling) {
|
if (isBilling) {
|
||||||
return redirect(getBillingFallbackPath(params.environmentId, IS_FORMBRICKS_CLOUD));
|
return redirect(getBillingFallbackPath(params.workspaceId, IS_FORMBRICKS_CLOUD));
|
||||||
}
|
}
|
||||||
|
|
||||||
const isGoogleSheetsIntegrationConnected = isIntegrationConnected("googleSheets");
|
const isGoogleSheetsIntegrationConnected = isIntegrationConnected("googleSheets");
|
||||||
@@ -64,7 +64,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
const isN8nIntegrationConnected = isIntegrationConnected("n8n");
|
const isN8nIntegrationConnected = isIntegrationConnected("n8n");
|
||||||
const isSlackIntegrationConnected = isIntegrationConnected("slack");
|
const isSlackIntegrationConnected = isIntegrationConnected("slack");
|
||||||
|
|
||||||
const appSetupCompleted = !!environment?.appSetupCompleted;
|
const appSetupCompleted = !!workspace.appSetupCompleted;
|
||||||
const integrationCards = [
|
const integrationCards = [
|
||||||
{
|
{
|
||||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/zapier",
|
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/zapier",
|
||||||
@@ -74,63 +74,63 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
connectText: t("common.connect"),
|
connectText: t("common.connect"),
|
||||||
connectNewTab: true,
|
connectNewTab: true,
|
||||||
label: "Zapier",
|
label: "Zapier",
|
||||||
description: t("environments.integrations.zapier_integration_description"),
|
description: t("workspace.integrations.zapier_integration_description"),
|
||||||
icon: <Image src={ZapierLogo} alt="Zapier Logo" />,
|
icon: <Image src={ZapierLogo} alt="Zapier Logo" />,
|
||||||
connected: zapierWebhookCount > 0,
|
connected: zapierWebhookCount > 0,
|
||||||
statusText: getStatusText(zapierWebhookCount, t, "zap"),
|
statusText: getStatusText(zapierWebhookCount, t, "zap"),
|
||||||
disabled: isReadOnly,
|
disabled: isReadOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connectHref: `/environments/${params.environmentId}/workspace/integrations/webhooks`,
|
connectHref: `/workspaces/${params.workspaceId}/integrations/webhooks`,
|
||||||
connectText: t("environments.integrations.manage_webhooks"),
|
connectText: t("workspace.integrations.manage_webhooks"),
|
||||||
connectNewTab: false,
|
connectNewTab: false,
|
||||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/webhooks",
|
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/webhooks",
|
||||||
docsText: t("common.docs"),
|
docsText: t("common.docs"),
|
||||||
docsNewTab: true,
|
docsNewTab: true,
|
||||||
label: "Webhooks",
|
label: "Webhooks",
|
||||||
description: t("environments.integrations.webhook_integration_description"),
|
description: t("workspace.integrations.webhook_integration_description"),
|
||||||
icon: <Image src={WebhookLogo} alt="Webhook Logo" />,
|
icon: <Image src={WebhookLogo} alt="Webhook Logo" />,
|
||||||
connected: userWebhookCount > 0,
|
connected: userWebhookCount > 0,
|
||||||
statusText: getStatusText(userWebhookCount, t, "webhook"),
|
statusText: getStatusText(userWebhookCount, t, "webhook"),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connectHref: `/environments/${params.environmentId}/workspace/integrations/google-sheets`,
|
connectHref: `/workspaces/${params.workspaceId}/integrations/google-sheets`,
|
||||||
connectText: `${isGoogleSheetsIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
connectText: `${isGoogleSheetsIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
||||||
connectNewTab: false,
|
connectNewTab: false,
|
||||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/google-sheets",
|
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/google-sheets",
|
||||||
docsText: t("common.docs"),
|
docsText: t("common.docs"),
|
||||||
docsNewTab: true,
|
docsNewTab: true,
|
||||||
label: "Google Sheets",
|
label: "Google Sheets",
|
||||||
description: t("environments.integrations.google_sheet_integration_description"),
|
description: t("workspace.integrations.google_sheet_integration_description"),
|
||||||
icon: <Image src={GoogleSheetsLogo} alt="Google sheets Logo" />,
|
icon: <Image src={GoogleSheetsLogo} alt="Google sheets Logo" />,
|
||||||
connected: isGoogleSheetsIntegrationConnected,
|
connected: isGoogleSheetsIntegrationConnected,
|
||||||
statusText: isGoogleSheetsIntegrationConnected ? t("common.connected") : t("common.not_connected"),
|
statusText: isGoogleSheetsIntegrationConnected ? t("common.connected") : t("common.not_connected"),
|
||||||
disabled: isReadOnly,
|
disabled: isReadOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connectHref: `/environments/${params.environmentId}/workspace/integrations/airtable`,
|
connectHref: `/workspaces/${params.workspaceId}/integrations/airtable`,
|
||||||
connectText: `${isAirtableIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
connectText: `${isAirtableIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
||||||
connectNewTab: false,
|
connectNewTab: false,
|
||||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/airtable",
|
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/airtable",
|
||||||
docsText: t("common.docs"),
|
docsText: t("common.docs"),
|
||||||
docsNewTab: true,
|
docsNewTab: true,
|
||||||
label: "Airtable",
|
label: "Airtable",
|
||||||
description: t("environments.integrations.airtable_integration_description"),
|
description: t("workspace.integrations.airtable_integration_description"),
|
||||||
icon: <Image src={AirtableLogo} alt="Airtable Logo" />,
|
icon: <Image src={AirtableLogo} alt="Airtable Logo" />,
|
||||||
connected: isAirtableIntegrationConnected,
|
connected: isAirtableIntegrationConnected,
|
||||||
statusText: isAirtableIntegrationConnected ? t("common.connected") : t("common.not_connected"),
|
statusText: isAirtableIntegrationConnected ? t("common.connected") : t("common.not_connected"),
|
||||||
disabled: isReadOnly,
|
disabled: isReadOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connectHref: `/environments/${params.environmentId}/workspace/integrations/slack`,
|
connectHref: `/workspaces/${params.workspaceId}/integrations/slack`,
|
||||||
connectText: `${isSlackIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
connectText: `${isSlackIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
||||||
connectNewTab: false,
|
connectNewTab: false,
|
||||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/slack",
|
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/slack",
|
||||||
docsText: t("common.docs"),
|
docsText: t("common.docs"),
|
||||||
docsNewTab: true,
|
docsNewTab: true,
|
||||||
label: "Slack",
|
label: "Slack",
|
||||||
description: t("environments.integrations.slack_integration_description"),
|
description: t("workspace.integrations.slack_integration_description"),
|
||||||
icon: <Image src={SlackLogo} alt="Slack Logo" />,
|
icon: <Image src={SlackLogo} alt="Slack Logo" />,
|
||||||
connected: isSlackIntegrationConnected,
|
connected: isSlackIntegrationConnected,
|
||||||
statusText: isSlackIntegrationConnected ? t("common.connected") : t("common.not_connected"),
|
statusText: isSlackIntegrationConnected ? t("common.connected") : t("common.not_connected"),
|
||||||
@@ -144,7 +144,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
connectHref: "https://n8n.io",
|
connectHref: "https://n8n.io",
|
||||||
connectNewTab: true,
|
connectNewTab: true,
|
||||||
label: "n8n",
|
label: "n8n",
|
||||||
description: t("environments.integrations.n8n_integration_description"),
|
description: t("workspace.integrations.n8n_integration_description"),
|
||||||
icon: <Image src={n8nLogo} alt="n8n Logo" />,
|
icon: <Image src={n8nLogo} alt="n8n Logo" />,
|
||||||
connected: n8nwebhookCount > 0,
|
connected: n8nwebhookCount > 0,
|
||||||
statusText: getStatusText(n8nwebhookCount, t, t("common.integration")),
|
statusText: getStatusText(n8nwebhookCount, t, t("common.integration")),
|
||||||
@@ -158,21 +158,21 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
connectText: t("common.connect"),
|
connectText: t("common.connect"),
|
||||||
connectNewTab: true,
|
connectNewTab: true,
|
||||||
label: "Make.com",
|
label: "Make.com",
|
||||||
description: t("environments.integrations.make_integration_description"),
|
description: t("workspace.integrations.make_integration_description"),
|
||||||
icon: <Image src={MakeLogo} alt="Make Logo" />,
|
icon: <Image src={MakeLogo} alt="Make Logo" />,
|
||||||
connected: makeWebhookCount > 0,
|
connected: makeWebhookCount > 0,
|
||||||
statusText: getStatusText(makeWebhookCount, t, t("common.integration")),
|
statusText: getStatusText(makeWebhookCount, t, t("common.integration")),
|
||||||
disabled: isReadOnly,
|
disabled: isReadOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connectHref: `/environments/${params.environmentId}/workspace/integrations/notion`,
|
connectHref: `/workspaces/${params.workspaceId}/integrations/notion`,
|
||||||
connectText: `${isNotionIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
connectText: `${isNotionIntegrationConnected ? t("common.manage") : t("common.connect")}`,
|
||||||
connectNewTab: false,
|
connectNewTab: false,
|
||||||
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/notion",
|
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/notion",
|
||||||
docsText: t("common.docs"),
|
docsText: t("common.docs"),
|
||||||
docsNewTab: true,
|
docsNewTab: true,
|
||||||
label: "Notion",
|
label: "Notion",
|
||||||
description: t("environments.integrations.notion_integration_description"),
|
description: t("workspace.integrations.notion_integration_description"),
|
||||||
icon: <Image src={notionLogo} alt="Notion Logo" />,
|
icon: <Image src={notionLogo} alt="Notion Logo" />,
|
||||||
connected: isNotionIntegrationConnected,
|
connected: isNotionIntegrationConnected,
|
||||||
statusText: isNotionIntegrationConnected ? t("common.connected") : t("common.not_connected"),
|
statusText: isNotionIntegrationConnected ? t("common.connected") : t("common.not_connected"),
|
||||||
@@ -186,7 +186,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
connectText: t("common.connect"),
|
connectText: t("common.connect"),
|
||||||
connectNewTab: true,
|
connectNewTab: true,
|
||||||
label: "Activepieces",
|
label: "Activepieces",
|
||||||
description: t("environments.integrations.activepieces_integration_description"),
|
description: t("workspace.integrations.activepieces_integration_description"),
|
||||||
icon: <Image src={ActivePiecesLogo} alt="ActivePieces Logo" />,
|
icon: <Image src={ActivePiecesLogo} alt="ActivePieces Logo" />,
|
||||||
connected: activePiecesWebhookCount > 0,
|
connected: activePiecesWebhookCount > 0,
|
||||||
statusText: getStatusText(activePiecesWebhookCount, t, t("common.integration")),
|
statusText: getStatusText(activePiecesWebhookCount, t, t("common.integration")),
|
||||||
@@ -198,11 +198,11 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
docsHref: "https://formbricks.com/docs/app-surveys/quickstart",
|
docsHref: "https://formbricks.com/docs/app-surveys/quickstart",
|
||||||
docsText: t("common.docs"),
|
docsText: t("common.docs"),
|
||||||
docsNewTab: true,
|
docsNewTab: true,
|
||||||
connectHref: `/environments/${params.environmentId}/workspace/app-connection`,
|
connectHref: `/workspaces/${params.workspaceId}/app-connection`,
|
||||||
connectText: t("common.connect"),
|
connectText: t("common.connect"),
|
||||||
connectNewTab: false,
|
connectNewTab: false,
|
||||||
label: "Javascript SDK",
|
label: "Javascript SDK",
|
||||||
description: t("environments.integrations.website_or_app_integration_description"),
|
description: t("workspace.integrations.website_or_app_integration_description"),
|
||||||
icon: <Image src={JsLogo} alt="Javascript Logo" />,
|
icon: <Image src={JsLogo} alt="Javascript Logo" />,
|
||||||
connected: appSetupCompleted,
|
connected: appSetupCompleted,
|
||||||
statusText: appSetupCompleted ? t("common.connected") : t("common.not_connected"),
|
statusText: appSetupCompleted ? t("common.connected") : t("common.not_connected"),
|
||||||
@@ -212,7 +212,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
return (
|
return (
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<PageHeader pageTitle={t("common.workspace_configuration")}>
|
<PageHeader pageTitle={t("common.workspace_configuration")}>
|
||||||
<ProjectConfigNavigation environmentId={params.environmentId} activeId="integrations" />
|
<WorkspaceConfigNavigation activeId="integrations" />
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<div className="grid grid-cols-3 place-content-stretch gap-4 lg:grid-cols-3">
|
<div className="grid grid-cols-3 place-content-stretch gap-4 lg:grid-cols-3">
|
||||||
{integrationCards.map((card) => (
|
{integrationCards.map((card) => (
|
||||||
+6
-6
@@ -5,10 +5,10 @@ import { ZId } from "@formbricks/types/common";
|
|||||||
import { getSlackChannels } from "@/lib/slack/service";
|
import { getSlackChannels } from "@/lib/slack/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||||
|
|
||||||
const ZGetSlackChannelsAction = z.object({
|
const ZGetSlackChannelsAction = z.object({
|
||||||
environmentId: ZId,
|
workspaceId: ZId,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getSlackChannelsAction = authenticatedActionClient
|
export const getSlackChannelsAction = authenticatedActionClient
|
||||||
@@ -16,19 +16,19 @@ export const getSlackChannelsAction = authenticatedActionClient
|
|||||||
.action(async ({ ctx, parsedInput }) => {
|
.action(async ({ ctx, parsedInput }) => {
|
||||||
await checkAuthorizationUpdated({
|
await checkAuthorizationUpdated({
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
organizationId: await getOrganizationIdFromWorkspaceId(parsedInput.workspaceId),
|
||||||
access: [
|
access: [
|
||||||
{
|
{
|
||||||
type: "organization",
|
type: "organization",
|
||||||
roles: ["owner", "manager"],
|
roles: ["owner", "manager"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "projectTeam",
|
type: "workspaceTeam",
|
||||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
workspaceId: parsedInput.workspaceId,
|
||||||
minPermission: "readWrite",
|
minPermission: "readWrite",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
return await getSlackChannels(parsedInput.environmentId);
|
return await getSlackChannels(parsedInput.workspaceId);
|
||||||
});
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user