mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 11:29:22 -05:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 022fa2f013 | |||
| 0a07970779 | |||
| 46be3e7d70 | |||
| 6d140532a7 | |||
| 8c4a7f1518 | |||
| 63fe32a786 | |||
| 84c465f974 | |||
| 6a33498737 | |||
| 5130c747d4 | |||
| f5583d2652 | |||
| e0d75914a4 | |||
| f02ca1cfe1 | |||
| 4ade83f189 | |||
| f1fc9fea2c | |||
| 25266e4566 | |||
| b960cfd2a1 | |||
| 9e1d1c1dc2 | |||
| 8c63a9f7af | |||
| fff0a7f052 | |||
| 0ecc8aabff | |||
| 01cc0ab64d | |||
| 1d125bdac2 | |||
| ca67c4d5a8 | |||
| d167d591ce | |||
| acc3b0179a | |||
| 3434b5cf08 | |||
| a618f2df95 | |||
| 5b334f6623 | |||
| fa2b63d6a1 | |||
| 9f0fe69b6b | |||
| 98cb2de02b | |||
| f00d0b7e20 | |||
| 65abd4ee07 | |||
| 939f135bf4 | |||
| 729a16854a | |||
| a2d3e37d69 | |||
| adf12f551d | |||
| 3f2bddc358 | |||
| ae6d1ac133 | |||
| 7c4569cd50 | |||
| 7354122447 | |||
| d54dca2b27 | |||
| acd5cff534 | |||
| 834929e766 | |||
| 09f40ad816 | |||
| 689b6491b3 | |||
| b70b2eef95 | |||
| 392a95834b | |||
| 66d9cc8eac | |||
| befdc078f1 | |||
| 13b983b3b2 | |||
| 1e285ebe4e | |||
| a7c4971952 | |||
| c8689d91d5 | |||
| 73a2ff7421 | |||
| 0c28e89b41 | |||
| a736436e29 | |||
| 7dbb0300d3 |
@@ -168,6 +168,9 @@ SLACK_CLIENT_SECRET=
|
|||||||
# Enterprise License Key
|
# Enterprise License Key
|
||||||
ENTERPRISE_LICENSE_KEY=
|
ENTERPRISE_LICENSE_KEY=
|
||||||
|
|
||||||
|
# Internal Environment (production, staging) - used for internal staging environment
|
||||||
|
# ENVIRONMENT=production
|
||||||
|
|
||||||
# Automatically assign new users to a specific organization and role within that organization
|
# Automatically assign new users to a specific organization and role within that organization
|
||||||
# Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
|
# Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
|
||||||
# (Role Management is an Enterprise feature)
|
# (Role Management is an Enterprise feature)
|
||||||
|
|||||||
@@ -13,13 +13,12 @@ jobs:
|
|||||||
chromatic:
|
chromatic:
|
||||||
name: Run Chromatic
|
name: Run Chromatic
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
contents: read
|
||||||
id-token: write
|
|
||||||
actions: read
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -27,16 +26,34 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||||
|
|
||||||
- name: Run Chromatic
|
- name: Run Chromatic
|
||||||
uses: chromaui/action@c93e0bc3a63aa176e14a75b61a31847cbfdd341c # latest
|
uses: chromaui/action@4c20b95e9d3209ecfdf9cd6aace6bbde71ba1694 # v13.3.4
|
||||||
with:
|
with:
|
||||||
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
|
|
||||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
workingDir: apps/storybook
|
workingDir: apps/storybook
|
||||||
|
zip: true
|
||||||
|
|||||||
@@ -203,6 +203,14 @@ Here are a few options:
|
|||||||
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
Formbricks is supported by the following companies who provide us with their tools for free as part of their open-source support:
|
||||||
|
|
||||||
|
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" width="153" height="30" alt="Chromatic" /></a>
|
||||||
|
|
||||||
|
<a href="https://sentry.io/"><img src="https://github.com/user-attachments/assets/d743ffd4-b575-4802-a29a-10136be9227e" width="150" height="30" alt="Sentry" /></a>
|
||||||
|
|
||||||
<a id="contact-us"></a>
|
<a id="contact-us"></a>
|
||||||
|
|
||||||
## 📆 Contact us
|
## 📆 Contact us
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const Page = async (props: ConnectPageProps) => {
|
|||||||
|
|
||||||
const project = await getProjectByEnvironmentId(environment.id);
|
const project = await getProjectByEnvironmentId(environment.id);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(t("common.project_not_found"));
|
throw new Error(t("common.workspace_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = project.config.channel || null;
|
const channel = project.config.channel || null;
|
||||||
@@ -46,7 +46,7 @@ const Page = async (props: ConnectPageProps) => {
|
|||||||
channel={channel}
|
channel={channel}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}`}>
|
<Link href={`/environments/${environment.id}`}>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const Page = async (props: XMTemplatePageProps) => {
|
|||||||
|
|
||||||
const project = await getProjectByEnvironmentId(environment.id);
|
const project = await getProjectByEnvironmentId(environment.id);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(t("common.project_not_found"));
|
throw new Error(t("common.workspace_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const projects = await getUserProjects(session.user.id, organizationId);
|
const projects = await getUserProjects(session.user.id, organizationId);
|
||||||
@@ -49,7 +49,7 @@ const Page = async (props: XMTemplatePageProps) => {
|
|||||||
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
||||||
{projects.length >= 2 && (
|
{projects.length >= 2 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}/surveys`}>
|
<Link href={`/environments/${environment.id}/surveys`}>
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ const Page = async (props) => {
|
|||||||
</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">
|
||||||
<Header
|
<Header
|
||||||
title={t("organizations.landing.no_projects_warning_title")}
|
title={t("organizations.landing.no_workspaces_warning_title")}
|
||||||
subtitle={t("organizations.landing.no_projects_warning_subtitle")}
|
subtitle={t("organizations.landing.no_workspaces_warning_subtitle")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+9
-9
@@ -26,16 +26,16 @@ const Page = async (props: ChannelPageProps) => {
|
|||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const channelOptions = [
|
const channelOptions = [
|
||||||
{
|
{
|
||||||
title: t("organizations.projects.new.channel.link_and_email_surveys"),
|
title: t("organizations.workspaces.new.channel.link_and_email_surveys"),
|
||||||
description: t("organizations.projects.new.channel.link_and_email_surveys_description"),
|
description: t("organizations.workspaces.new.channel.link_and_email_surveys_description"),
|
||||||
icon: SendIcon,
|
icon: SendIcon,
|
||||||
href: `/organizations/${params.organizationId}/projects/new/settings?channel=link`,
|
href: `/organizations/${params.organizationId}/workspaces/new/settings?channel=link`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("organizations.projects.new.channel.in_product_surveys"),
|
title: t("organizations.workspaces.new.channel.in_product_surveys"),
|
||||||
description: t("organizations.projects.new.channel.in_product_surveys_description"),
|
description: t("organizations.workspaces.new.channel.in_product_surveys_description"),
|
||||||
icon: PictureInPicture2Icon,
|
icon: PictureInPicture2Icon,
|
||||||
href: `/organizations/${params.organizationId}/projects/new/settings?channel=app`,
|
href: `/organizations/${params.organizationId}/workspaces/new/settings?channel=app`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -44,13 +44,13 @@ const Page = async (props: ChannelPageProps) => {
|
|||||||
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
|
<Header
|
||||||
title={t("organizations.projects.new.channel.channel_select_title")}
|
title={t("organizations.workspaces.new.channel.channel_select_title")}
|
||||||
subtitle={t("organizations.projects.new.channel.channel_select_subtitle")}
|
subtitle={t("organizations.workspaces.new.channel.channel_select_subtitle")}
|
||||||
/>
|
/>
|
||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
+1
-1
@@ -15,7 +15,7 @@ const OnboardingLayout = async (props) => {
|
|||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session || !session.user) {
|
if (!session?.user) {
|
||||||
return redirect(`/auth/login`);
|
return redirect(`/auth/login`);
|
||||||
}
|
}
|
||||||
|
|
||||||
+8
-8
@@ -26,16 +26,16 @@ const Page = async (props: ModePageProps) => {
|
|||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const channelOptions = [
|
const channelOptions = [
|
||||||
{
|
{
|
||||||
title: t("organizations.projects.new.mode.formbricks_surveys"),
|
title: t("organizations.workspaces.new.mode.formbricks_surveys"),
|
||||||
description: t("organizations.projects.new.mode.formbricks_surveys_description"),
|
description: t("organizations.workspaces.new.mode.formbricks_surveys_description"),
|
||||||
icon: ListTodoIcon,
|
icon: ListTodoIcon,
|
||||||
href: `/organizations/${params.organizationId}/projects/new/channel`,
|
href: `/organizations/${params.organizationId}/workspaces/new/channel`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("organizations.projects.new.mode.formbricks_cx"),
|
title: t("organizations.workspaces.new.mode.formbricks_cx"),
|
||||||
description: t("organizations.projects.new.mode.formbricks_cx_description"),
|
description: t("organizations.workspaces.new.mode.formbricks_cx_description"),
|
||||||
icon: HeartIcon,
|
icon: HeartIcon,
|
||||||
href: `/organizations/${params.organizationId}/projects/new/settings?mode=cx`,
|
href: `/organizations/${params.organizationId}/workspaces/new/settings?mode=cx`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -43,11 +43,11 @@ const Page = async (props: ModePageProps) => {
|
|||||||
|
|
||||||
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.projects.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 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
+9
-10
@@ -79,7 +79,7 @@ export const ProjectSettings = ({
|
|||||||
(environment) => environment.type === "production"
|
(environment) => environment.type === "production"
|
||||||
);
|
);
|
||||||
if (productionEnvironment) {
|
if (productionEnvironment) {
|
||||||
if (typeof window !== "undefined") {
|
if (globalThis.window !== undefined) {
|
||||||
// Rmove filters when creating a new project
|
// Rmove filters when creating a new project
|
||||||
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ export const ProjectSettings = ({
|
|||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t("organizations.projects.new.settings.project_creation_failed"));
|
toast.error(t("organizations.workspaces.new.settings.workspace_creation_failed"));
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -107,7 +107,6 @@ export const ProjectSettings = ({
|
|||||||
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
|
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
|
||||||
teamIds: [],
|
teamIds: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
resolver: zodResolver(ZProjectUpdateInput),
|
resolver: zodResolver(ZProjectUpdateInput),
|
||||||
});
|
});
|
||||||
const projectName = form.watch("name");
|
const projectName = form.watch("name");
|
||||||
@@ -131,9 +130,9 @@ export const ProjectSettings = ({
|
|||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormItem className="w-full space-y-4">
|
<FormItem className="w-full space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<FormLabel>{t("organizations.projects.new.settings.brand_color")}</FormLabel>
|
<FormLabel>{t("organizations.workspaces.new.settings.brand_color")}</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t("organizations.projects.new.settings.brand_color_description")}
|
{t("organizations.workspaces.new.settings.brand_color_description")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -155,9 +154,9 @@ export const ProjectSettings = ({
|
|||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormItem className="w-full space-y-4">
|
<FormItem className="w-full space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<FormLabel>{t("organizations.projects.new.settings.project_name")}</FormLabel>
|
<FormLabel>{t("organizations.workspaces.new.settings.workspace_name")}</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t("organizations.projects.new.settings.project_name_description")}
|
{t("organizations.workspaces.new.settings.workspace_name_description")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -186,7 +185,7 @@ export const ProjectSettings = ({
|
|||||||
<div>
|
<div>
|
||||||
<FormLabel>{t("common.teams")}</FormLabel>
|
<FormLabel>{t("common.teams")}</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t("organizations.projects.new.settings.team_description")}
|
{t("organizations.workspaces.new.settings.team_description")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -194,7 +193,7 @@ export const ProjectSettings = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setCreateTeamModalOpen(true)}>
|
onClick={() => setCreateTeamModalOpen(true)}>
|
||||||
{t("organizations.projects.new.settings.create_new_team")}
|
{t("organizations.workspaces.new.settings.create_new_team")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -227,7 +226,7 @@ export const ProjectSettings = ({
|
|||||||
alt="Logo"
|
alt="Logo"
|
||||||
width={256}
|
width={256}
|
||||||
height={56}
|
height={56}
|
||||||
className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
className="absolute top-2 left-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
||||||
+4
-4
@@ -3,7 +3,7 @@ import Link from "next/link";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
||||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/settings/components/ProjectSettings";
|
||||||
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 { getUserProjects } from "@/lib/project/service";
|
||||||
@@ -53,8 +53,8 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
|||||||
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
|
<Header
|
||||||
title={t("organizations.projects.new.settings.project_settings_title")}
|
title={t("organizations.workspaces.new.settings.workspace_settings_title")}
|
||||||
subtitle={t("organizations.projects.new.settings.project_settings_subtitle")}
|
subtitle={t("organizations.workspaces.new.settings.workspace_settings_subtitle")}
|
||||||
/>
|
/>
|
||||||
<ProjectSettings
|
<ProjectSettings
|
||||||
organizationId={params.organizationId}
|
organizationId={params.organizationId}
|
||||||
@@ -69,7 +69,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
|||||||
/>
|
/>
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { AttributesPage as default } from "@/modules/ee/contacts/attributes/page";
|
||||||
@@ -57,7 +57,7 @@ export const createProjectAction = authenticatedActionClient.schema(ZCreateProje
|
|||||||
const organizationProjectsCount = await getOrganizationProjectsCount(organization.id);
|
const organizationProjectsCount = await getOrganizationProjectsCount(organization.id);
|
||||||
|
|
||||||
if (organizationProjectsCount >= organizationProjectsLimit) {
|
if (organizationProjectsCount >= organizationProjectsLimit) {
|
||||||
throw new OperationNotAllowedError("Organization project limit reached");
|
throw new OperationNotAllowedError("Organization workspace limit reached");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedInput.data.teamIds && parsedInput.data.teamIds.length > 0) {
|
if (parsedInput.data.teamIds && parsedInput.data.teamIds.length > 0) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
|
|||||||
|
|
||||||
// Validate that project permission exists for members
|
// Validate that project permission exists for members
|
||||||
if (isMember && !projectPermission) {
|
if (isMember && !projectPermission) {
|
||||||
throw new Error(t("common.project_permission_not_found"));
|
throw new Error(t("common.workspace_permission_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export const MainNavigation = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("common.configuration"),
|
name: t("common.configuration"),
|
||||||
href: `/environments/${environment.id}/project/general`,
|
href: `/environments/${environment.id}/workspace/general`,
|
||||||
icon: Cog,
|
icon: Cog,
|
||||||
isActive: pathname?.includes("/project"),
|
isActive: pathname?.includes("/project"),
|
||||||
},
|
},
|
||||||
@@ -164,7 +164,7 @@ export const MainNavigation = ({
|
|||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-40 flex 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 flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100",
|
||||||
!isCollapsed ? "w-sidebar-collapsed" : "w-sidebar-expanded"
|
isCollapsed ? "w-sidebar-expanded" : "w-sidebar-collapsed"
|
||||||
)}>
|
)}>
|
||||||
<div>
|
<div>
|
||||||
{/* Logo and Toggle */}
|
{/* Logo and Toggle */}
|
||||||
@@ -185,7 +185,7 @@ export const MainNavigation = ({
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:outline-none focus:ring-0 focus:ring-transparent"
|
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-none"
|
||||||
)}>
|
)}>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
<PanelLeftOpenIcon strokeWidth={1.5} />
|
<PanelLeftOpenIcon strokeWidth={1.5} />
|
||||||
|
|||||||
+6
-6
@@ -17,13 +17,13 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
|
|||||||
const stati = {
|
const stati = {
|
||||||
notImplemented: {
|
notImplemented: {
|
||||||
icon: AlertTriangleIcon,
|
icon: AlertTriangleIcon,
|
||||||
title: t("environments.project.app-connection.formbricks_sdk_not_connected"),
|
title: t("environments.workspace.app-connection.formbricks_sdk_not_connected"),
|
||||||
subtitle: t("environments.project.app-connection.formbricks_sdk_not_connected_description"),
|
subtitle: t("environments.workspace.app-connection.formbricks_sdk_not_connected_description"),
|
||||||
},
|
},
|
||||||
running: {
|
running: {
|
||||||
icon: CheckIcon,
|
icon: CheckIcon,
|
||||||
title: t("environments.project.app-connection.receiving_data"),
|
title: t("environments.workspace.app-connection.receiving_data"),
|
||||||
subtitle: t("environments.project.app-connection.formbricks_sdk_connected"),
|
subtitle: t("environments.workspace.app-connection.formbricks_sdk_connected"),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,11 +53,11 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
|
|||||||
<currentStatus.icon />
|
<currentStatus.icon />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
||||||
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
|
<p className="w-2/3 text-sm text-balance text-slate-600">{currentStatus.subtitle}</p>
|
||||||
{status === "notImplemented" && (
|
{status === "notImplemented" && (
|
||||||
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
||||||
<RotateCcwIcon />
|
<RotateCcwIcon />
|
||||||
{t("environments.project.app-connection.recheck")}
|
{t("environments.workspace.app-connection.recheck")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+9
-3
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
import {
|
import {
|
||||||
BuildingIcon,
|
Building2Icon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -144,6 +144,12 @@ export const OrganizationBreadcrumb = ({
|
|||||||
href: `/environments/${currentEnvironmentId}/settings/api-keys`,
|
href: `/environments/${currentEnvironmentId}/settings/api-keys`,
|
||||||
hidden: !isOwnerOrManager,
|
hidden: !isOwnerOrManager,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "domain",
|
||||||
|
label: t("common.domain"),
|
||||||
|
href: `/environments/${currentEnvironmentId}/settings/domain`,
|
||||||
|
hidden: isFormbricksCloud,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "billing",
|
id: "billing",
|
||||||
label: t("common.billing"),
|
label: t("common.billing"),
|
||||||
@@ -166,7 +172,7 @@ export const OrganizationBreadcrumb = ({
|
|||||||
id="organizationDropdownTrigger"
|
id="organizationDropdownTrigger"
|
||||||
asChild>
|
asChild>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<BuildingIcon className="h-3 w-3" strokeWidth={1.5} />
|
<Building2Icon className="h-3 w-3" strokeWidth={1.5} />
|
||||||
<span>{organizationName}</span>
|
<span>{organizationName}</span>
|
||||||
{isPending && <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1.5} />}
|
{isPending && <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1.5} />}
|
||||||
{isOrganizationDropdownOpen ? (
|
{isOrganizationDropdownOpen ? (
|
||||||
@@ -180,7 +186,7 @@ export const OrganizationBreadcrumb = ({
|
|||||||
{showOrganizationDropdown && (
|
{showOrganizationDropdown && (
|
||||||
<>
|
<>
|
||||||
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
||||||
<BuildingIcon className="mr-2 inline h-4 w-4" />
|
<Building2Icon className="mr-2 inline h-4 w-4" />
|
||||||
{t("common.choose_organization")}
|
{t("common.choose_organization")}
|
||||||
</div>
|
</div>
|
||||||
{isLoadingOrganizations && (
|
{isLoadingOrganizations && (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
import { ChevronDownIcon, ChevronRightIcon, CogIcon, FolderOpenIcon, Loader2, PlusIcon } from "lucide-react";
|
import { ChevronDownIcon, ChevronRightIcon, CogIcon, HotelIcon, Loader2, PlusIcon } from "lucide-react";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useEffect, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -36,12 +36,12 @@ interface ProjectBreadcrumbProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isActiveProjectSetting = (pathname: string, settingId: string): boolean => {
|
const isActiveProjectSetting = (pathname: string, settingId: string): boolean => {
|
||||||
// Match /project/{settingId} or /project/{settingId}/... but exclude settings paths
|
// Match /workspace/{settingId} or /workspace/{settingId}/... but exclude settings paths
|
||||||
if (pathname.includes("/settings/")) {
|
if (pathname.includes("/settings/")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Check if path matches /project/{settingId} (with optional trailing path)
|
// Check if path matches /workspace/{settingId} (with optional trailing path)
|
||||||
const pattern = new RegExp(`/project/${settingId}(?:/|$)`);
|
const pattern = new RegExp(`/workspace/${settingId}(?:/|$)`);
|
||||||
return pattern.test(pathname);
|
return pattern.test(pathname);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ export const ProjectBreadcrumb = ({
|
|||||||
const error = new Error(errorMessage);
|
const error = new Error(errorMessage);
|
||||||
logger.error(error, "Failed to load projects");
|
logger.error(error, "Failed to load projects");
|
||||||
Sentry.captureException(error);
|
Sentry.captureException(error);
|
||||||
setLoadError(errorMessage || t("common.failed_to_load_projects"));
|
setLoadError(errorMessage || t("common.failed_to_load_workspaces"));
|
||||||
}
|
}
|
||||||
setIsLoadingProjects(false);
|
setIsLoadingProjects(false);
|
||||||
});
|
});
|
||||||
@@ -101,42 +101,42 @@ export const ProjectBreadcrumb = ({
|
|||||||
{
|
{
|
||||||
id: "general",
|
id: "general",
|
||||||
label: t("common.general"),
|
label: t("common.general"),
|
||||||
href: `/environments/${currentEnvironmentId}/project/general`,
|
href: `/environments/${currentEnvironmentId}/workspace/general`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "look",
|
id: "look",
|
||||||
label: t("common.look_and_feel"),
|
label: t("common.look_and_feel"),
|
||||||
href: `/environments/${currentEnvironmentId}/project/look`,
|
href: `/environments/${currentEnvironmentId}/workspace/look`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "app-connection",
|
id: "app-connection",
|
||||||
label: t("common.website_and_app_connection"),
|
label: t("common.website_and_app_connection"),
|
||||||
href: `/environments/${currentEnvironmentId}/project/app-connection`,
|
href: `/environments/${currentEnvironmentId}/workspace/app-connection`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "integrations",
|
id: "integrations",
|
||||||
label: t("common.integrations"),
|
label: t("common.integrations"),
|
||||||
href: `/environments/${currentEnvironmentId}/project/integrations`,
|
href: `/environments/${currentEnvironmentId}/workspace/integrations`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "teams",
|
id: "teams",
|
||||||
label: t("common.team_access"),
|
label: t("common.team_access"),
|
||||||
href: `/environments/${currentEnvironmentId}/project/teams`,
|
href: `/environments/${currentEnvironmentId}/workspace/teams`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "languages",
|
id: "languages",
|
||||||
label: t("common.survey_languages"),
|
label: t("common.survey_languages"),
|
||||||
href: `/environments/${currentEnvironmentId}/project/languages`,
|
href: `/environments/${currentEnvironmentId}/workspace/languages`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "tags",
|
id: "tags",
|
||||||
label: t("common.tags"),
|
label: t("common.tags"),
|
||||||
href: `/environments/${currentEnvironmentId}/project/tags`,
|
href: `/environments/${currentEnvironmentId}/workspace/tags`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!currentProject) {
|
if (!currentProject) {
|
||||||
const errorMessage = `Project not found for project id: ${currentProjectId}`;
|
const errorMessage = `Workspace not found for workspace id: ${currentProjectId}`;
|
||||||
logger.error(errorMessage);
|
logger.error(errorMessage);
|
||||||
Sentry.captureException(new Error(errorMessage));
|
Sentry.captureException(new Error(errorMessage));
|
||||||
return;
|
return;
|
||||||
@@ -145,7 +145,7 @@ export const ProjectBreadcrumb = ({
|
|||||||
const handleProjectChange = (projectId: string) => {
|
const handleProjectChange = (projectId: string) => {
|
||||||
if (projectId === currentProjectId) return;
|
if (projectId === currentProjectId) return;
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
router.push(`/projects/${projectId}/`);
|
router.push(`/workspaces/${projectId}/`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ export const ProjectBreadcrumb = ({
|
|||||||
|
|
||||||
const handleProjectSettingsNavigation = (settingId: string) => {
|
const handleProjectSettingsNavigation = (settingId: string) => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
router.push(`/environments/${currentEnvironmentId}/project/${settingId}`);
|
router.push(`/environments/${currentEnvironmentId}/workspace/${settingId}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,21 +198,21 @@ export const ProjectBreadcrumb = ({
|
|||||||
id="projectDropdownTrigger"
|
id="projectDropdownTrigger"
|
||||||
asChild>
|
asChild>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<FolderOpenIcon className="h-3 w-3" strokeWidth={1.5} />
|
<HotelIcon className="h-3 w-3" strokeWidth={1.5} />
|
||||||
<span>{projectName}</span>
|
<span>{projectName}</span>
|
||||||
{isPending && <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1.5} />}
|
{isPending && <Loader2 className="h-3 w-3 animate-spin" strokeWidth={1.5} />}
|
||||||
{isProjectDropdownOpen ? (
|
{isEnvironmentBreadcrumbVisible && !isProjectDropdownOpen ? (
|
||||||
<ChevronDownIcon className="h-3 w-3" strokeWidth={1.5} />
|
<ChevronRightIcon className="h-3 w-3" strokeWidth={1.5} />
|
||||||
) : (
|
) : (
|
||||||
isEnvironmentBreadcrumbVisible && <ChevronRightIcon className="h-3 w-3" strokeWidth={1.5} />
|
<ChevronDownIcon className="h-3 w-3" strokeWidth={1.5} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent align="start" className="mt-2">
|
<DropdownMenuContent align="start" className="mt-2">
|
||||||
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
||||||
<FolderOpenIcon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
<HotelIcon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
||||||
{t("common.choose_project")}
|
{t("common.choose_workspace")}
|
||||||
</div>
|
</div>
|
||||||
{isLoadingProjects && (
|
{isLoadingProjects && (
|
||||||
<div className="flex items-center justify-center py-2">
|
<div className="flex items-center justify-center py-2">
|
||||||
@@ -251,7 +251,7 @@ export const ProjectBreadcrumb = ({
|
|||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
onClick={handleAddProject}
|
onClick={handleAddProject}
|
||||||
className="w-full cursor-pointer justify-between">
|
className="w-full cursor-pointer justify-between">
|
||||||
<span>{t("common.add_new_project")}</span>
|
<span>{t("common.add_new_workspace")}</span>
|
||||||
<PlusIcon className="ml-2 h-4 w-4" strokeWidth={1.5} />
|
<PlusIcon className="ml-2 h-4 w-4" strokeWidth={1.5} />
|
||||||
</DropdownMenuCheckboxItem>
|
</DropdownMenuCheckboxItem>
|
||||||
)}
|
)}
|
||||||
@@ -261,7 +261,7 @@ export const ProjectBreadcrumb = ({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
<div className="px-2 py-1.5 text-sm font-medium text-slate-500">
|
||||||
<CogIcon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
<CogIcon className="mr-2 inline h-4 w-4" strokeWidth={1.5} />
|
||||||
{t("common.project_configuration")}
|
{t("common.workspace_configuration")}
|
||||||
</div>
|
</div>
|
||||||
{projectSettings.map((setting) => (
|
{projectSettings.map((setting) => (
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
|
|||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
|
||||||
|
|
||||||
export const TYPE_MAPPING = {
|
|
||||||
[TSurveyQuestionTypeEnum.CTA]: ["checkbox"],
|
|
||||||
[TSurveyQuestionTypeEnum.MultipleChoiceMulti]: ["multi_select"],
|
|
||||||
[TSurveyQuestionTypeEnum.MultipleChoiceSingle]: ["select", "status"],
|
|
||||||
[TSurveyQuestionTypeEnum.OpenText]: [
|
|
||||||
"created_by",
|
|
||||||
"created_time",
|
|
||||||
"email",
|
|
||||||
"last_edited_by",
|
|
||||||
"last_edited_time",
|
|
||||||
"number",
|
|
||||||
"phone_number",
|
|
||||||
"rich_text",
|
|
||||||
"title",
|
|
||||||
"url",
|
|
||||||
],
|
|
||||||
[TSurveyQuestionTypeEnum.NPS]: ["number"],
|
|
||||||
[TSurveyQuestionTypeEnum.Consent]: ["checkbox"],
|
|
||||||
[TSurveyQuestionTypeEnum.Rating]: ["number"],
|
|
||||||
[TSurveyQuestionTypeEnum.PictureSelection]: ["url"],
|
|
||||||
[TSurveyQuestionTypeEnum.FileUpload]: ["url"],
|
|
||||||
[TSurveyQuestionTypeEnum.Date]: ["date"],
|
|
||||||
[TSurveyQuestionTypeEnum.Address]: ["rich_text"],
|
|
||||||
[TSurveyQuestionTypeEnum.Matrix]: ["rich_text"],
|
|
||||||
[TSurveyQuestionTypeEnum.Cal]: ["checkbox"],
|
|
||||||
[TSurveyQuestionTypeEnum.ContactInfo]: ["rich_text"],
|
|
||||||
[TSurveyQuestionTypeEnum.Ranking]: ["rich_text"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UNSUPPORTED_TYPES_BY_NOTION = [
|
|
||||||
"rollup",
|
|
||||||
"created_by",
|
|
||||||
"created_time",
|
|
||||||
"last_edited_by",
|
|
||||||
"last_edited_time",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ERRORS = {
|
|
||||||
MAPPING: "Mapping Error",
|
|
||||||
UNSUPPORTED_TYPE: "Unsupported type by Notion",
|
|
||||||
};
|
|
||||||
@@ -21,7 +21,7 @@ const AccountSettingsLayout = async (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(t("common.project_not_found"));
|
throw new Error(t("common.workspace_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ export const IntegrationsTip = ({ environmentId }: IntegrationsTipProps) => {
|
|||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{t("environments.settings.notifications.need_slack_or_discord_notifications")}?
|
{t("environments.settings.notifications.need_slack_or_discord_notifications")}?
|
||||||
<a
|
<a
|
||||||
href={`/environments/${environmentId}/project/integrations`}
|
href={`/environments/${environmentId}/workspace/integrations`}
|
||||||
className="ml-1 cursor-pointer text-sm underline">
|
className="ml-1 cursor-pointer text-sm underline">
|
||||||
{t("environments.settings.notifications.use_the_integration")}
|
{t("environments.settings.notifications.use_the_integration")}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
+7
@@ -47,6 +47,13 @@ export const OrganizationSettingsNavbar = ({
|
|||||||
current: pathname?.includes("/api-keys"),
|
current: pathname?.includes("/api-keys"),
|
||||||
hidden: !isOwner,
|
hidden: !isOwner,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "domain",
|
||||||
|
label: t("common.domain"),
|
||||||
|
href: `/environments/${environmentId}/settings/domain`,
|
||||||
|
current: pathname?.includes("/domain"),
|
||||||
|
hidden: isFormbricksCloud,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "billing",
|
id: "billing",
|
||||||
label: t("common.billing"),
|
label: t("common.billing"),
|
||||||
|
|||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { TSurveyStatus } from "@formbricks/types/surveys/types";
|
||||||
|
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
|
||||||
|
|
||||||
|
interface SurveyWithSlug {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string | null;
|
||||||
|
status: TSurveyStatus;
|
||||||
|
environment: {
|
||||||
|
id: string;
|
||||||
|
type: "production" | "development";
|
||||||
|
project: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PrettyUrlsTableProps {
|
||||||
|
surveys: SurveyWithSlug[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrettyUrlsTable = ({ surveys }: PrettyUrlsTableProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const getEnvironmentBadgeColor = (type: string) => {
|
||||||
|
return type === "production" ? "bg-green-100 text-green-800" : "bg-blue-100 text-blue-800";
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableHeaders = [
|
||||||
|
{
|
||||||
|
label: t("environments.settings.domain.survey_name"),
|
||||||
|
key: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("environments.settings.domain.workspace"),
|
||||||
|
key: "project",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("environments.settings.domain.pretty_url"),
|
||||||
|
key: "slug",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("common.environment"),
|
||||||
|
key: "environment",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-lg">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-slate-100">
|
||||||
|
{tableHeaders.map((header) => (
|
||||||
|
<TableHead key={header.key} className="font-medium text-slate-500">
|
||||||
|
{header.label}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody className="[&_tr:last-child]:border-b">
|
||||||
|
{surveys.length === 0 && (
|
||||||
|
<TableRow className="hover:bg-transparent">
|
||||||
|
<TableCell colSpan={4} className="text-center text-slate-500">
|
||||||
|
{t("environments.settings.domain.no_pretty_urls")}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{surveys.map((survey) => (
|
||||||
|
<TableRow key={survey.id} className="border-slate-200 hover:bg-transparent">
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
<Link
|
||||||
|
href={`/environments/${survey.environment.id}/surveys/${survey.id}/summary`}
|
||||||
|
className="text-slate-900 hover:text-slate-700 hover:underline">
|
||||||
|
{survey.name}
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{survey.environment.project.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<IdBadge id={survey.slug ?? ""} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span
|
||||||
|
className={`rounded px-2 py-1 text-xs font-medium ${getEnvironmentBadgeColor(survey.environment.type)}`}>
|
||||||
|
{survey.environment.type === "production"
|
||||||
|
? t("common.production")
|
||||||
|
: t("common.development")}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
|
||||||
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
|
import { getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
||||||
|
import { FaviconCustomizationSettings } from "@/modules/ee/whitelabel/favicon-customization/components/favicon-customization-settings";
|
||||||
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
|
import { getSurveysWithSlugsByOrganizationId } from "@/modules/survey/lib/slug";
|
||||||
|
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||||
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
|
import { SettingsCard } from "../../components/SettingsCard";
|
||||||
|
import { OrganizationSettingsNavbar } from "../components/OrganizationSettingsNavbar";
|
||||||
|
import { PrettyUrlsTable } from "./components/pretty-urls-table";
|
||||||
|
|
||||||
|
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||||
|
const params = await props.params;
|
||||||
|
const t = await getTranslate();
|
||||||
|
|
||||||
|
if (IS_FORMBRICKS_CLOUD) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { session, currentUserMembership, organization, isOwner, isManager } = await getEnvironmentAuth(
|
||||||
|
params.environmentId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new Error(t("common.session_not_found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasWhiteLabelPermission = await getWhiteLabelPermission(organization.billing.plan);
|
||||||
|
const isOwnerOrManager = isManager || isOwner;
|
||||||
|
|
||||||
|
const surveys = await getSurveysWithSlugsByOrganizationId(organization.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContentWrapper>
|
||||||
|
<PageHeader pageTitle={t("environments.settings.general.organization_settings")}>
|
||||||
|
<OrganizationSettingsNavbar
|
||||||
|
environmentId={params.environmentId}
|
||||||
|
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||||
|
membershipRole={currentUserMembership?.role}
|
||||||
|
activeId="domain"
|
||||||
|
/>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
{!IS_STORAGE_CONFIGURED && (
|
||||||
|
<div className="max-w-4xl">
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertDescription>{t("common.storage_not_configured")}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FaviconCustomizationSettings
|
||||||
|
organization={organization}
|
||||||
|
hasWhiteLabelPermission={hasWhiteLabelPermission}
|
||||||
|
environmentId={params.environmentId}
|
||||||
|
isReadOnly={!isOwnerOrManager}
|
||||||
|
isStorageConfigured={IS_STORAGE_CONFIGURED}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingsCard
|
||||||
|
title={t("environments.settings.domain.title")}
|
||||||
|
description={t("environments.settings.domain.description")}>
|
||||||
|
<PrettyUrlsTable surveys={surveys} />
|
||||||
|
</SettingsCard>
|
||||||
|
</PageContentWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
+2
-2
@@ -39,7 +39,7 @@ const Page = async (props) => {
|
|||||||
onRequest: false,
|
onRequest: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("environments.project.languages.multi_language_surveys"),
|
title: t("environments.workspace.languages.multi_language_surveys"),
|
||||||
comingSoon: false,
|
comingSoon: false,
|
||||||
onRequest: false,
|
onRequest: false,
|
||||||
},
|
},
|
||||||
@@ -118,7 +118,7 @@ const Page = async (props) => {
|
|||||||
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
|
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 1024 1024"
|
viewBox="0 0 1024 1024"
|
||||||
className="absolute left-1/2 top-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
|
className="absolute top-1/2 left-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<circle
|
<circle
|
||||||
cx={512}
|
cx={512}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const Layout = async (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(t("common.project_not_found"));
|
throw new Error(t("common.workspace_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ export const EmptyAppSurveys = ({ environment }: TEmptyAppSurveysProps) => {
|
|||||||
{t("environments.surveys.summary.connect_your_website_or_app_with_formbricks_to_get_started")}
|
{t("environments.surveys.summary.connect_your_website_or_app_with_formbricks_to_get_started")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Link className="mt-2" href={`/environments/${environment.id}/project/app-connection`}>
|
<Link className="mt-2" href={`/environments/${environment.id}/workspace/app-connection`}>
|
||||||
<Button size="sm" className="flex w-[120px] justify-center">
|
<Button size="sm" className="flex w-[120px] justify-center">
|
||||||
{t("common.connect")}
|
{t("common.connect")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+1
@@ -213,6 +213,7 @@ export const SurveyAnalysisCTA = ({
|
|||||||
isFormbricksCloud={isFormbricksCloud}
|
isFormbricksCloud={isFormbricksCloud}
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
isStorageConfigured={isStorageConfigured}
|
isStorageConfigured={isStorageConfigured}
|
||||||
|
projectCustomScripts={project.customHeadScripts}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SuccessMessage environment={environment} survey={survey} />
|
<SuccessMessage environment={environment} survey={survey} />
|
||||||
|
|||||||
+52
-20
@@ -3,7 +3,8 @@
|
|||||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||||
import {
|
import {
|
||||||
Code2Icon,
|
Code2Icon,
|
||||||
LinkIcon,
|
CodeIcon,
|
||||||
|
Link2Icon,
|
||||||
MailIcon,
|
MailIcon,
|
||||||
QrCodeIcon,
|
QrCodeIcon,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -18,10 +19,12 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
|||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import { AnonymousLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab";
|
import { AnonymousLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/anonymous-links-tab";
|
||||||
import { AppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab";
|
import { AppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/app-tab";
|
||||||
|
import { CustomHtmlTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/custom-html-tab";
|
||||||
import { DynamicPopupTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab";
|
import { DynamicPopupTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/dynamic-popup-tab";
|
||||||
import { EmailTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab";
|
import { EmailTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab";
|
||||||
import { LinkSettingsTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/link-settings-tab";
|
import { LinkSettingsTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/link-settings-tab";
|
||||||
import { PersonalLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab";
|
import { PersonalLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab";
|
||||||
|
import { PrettyUrlTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/pretty-url-tab";
|
||||||
import { QRCodeTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab";
|
import { QRCodeTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab";
|
||||||
import { SocialMediaTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab";
|
import { SocialMediaTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab";
|
||||||
import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container";
|
import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container";
|
||||||
@@ -50,6 +53,7 @@ interface ShareSurveyModalProps {
|
|||||||
isFormbricksCloud: boolean;
|
isFormbricksCloud: boolean;
|
||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
isStorageConfigured: boolean;
|
isStorageConfigured: boolean;
|
||||||
|
projectCustomScripts?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShareSurveyModal = ({
|
export const ShareSurveyModal = ({
|
||||||
@@ -64,6 +68,7 @@ export const ShareSurveyModal = ({
|
|||||||
isFormbricksCloud,
|
isFormbricksCloud,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
isStorageConfigured,
|
isStorageConfigured,
|
||||||
|
projectCustomScripts,
|
||||||
}: ShareSurveyModalProps) => {
|
}: ShareSurveyModalProps) => {
|
||||||
const environmentId = survey.environmentId;
|
const environmentId = survey.environmentId;
|
||||||
const [surveyUrl, setSurveyUrl] = useState<string>(getSurveyUrl(survey, publicDomain, "default"));
|
const [surveyUrl, setSurveyUrl] = useState<string>(getSurveyUrl(survey, publicDomain, "default"));
|
||||||
@@ -80,13 +85,13 @@ export const ShareSurveyModal = ({
|
|||||||
componentType: React.ComponentType<unknown>;
|
componentType: React.ComponentType<unknown>;
|
||||||
componentProps: unknown;
|
componentProps: unknown;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}[] = useMemo(
|
}[] = useMemo(() => {
|
||||||
() => [
|
const tabs = [
|
||||||
{
|
{
|
||||||
id: ShareViaType.ANON_LINKS,
|
id: ShareViaType.ANON_LINKS,
|
||||||
type: LinkTabsType.SHARE_VIA,
|
type: LinkTabsType.SHARE_VIA,
|
||||||
label: t("environments.surveys.share.anonymous_links.nav_title"),
|
label: t("environments.surveys.share.anonymous_links.nav_title"),
|
||||||
icon: LinkIcon,
|
icon: Link2Icon,
|
||||||
title: t("environments.surveys.share.anonymous_links.nav_title"),
|
title: t("environments.surveys.share.anonymous_links.nav_title"),
|
||||||
description: t("environments.surveys.share.anonymous_links.description"),
|
description: t("environments.surveys.share.anonymous_links.description"),
|
||||||
componentType: AnonymousLinksTab,
|
componentType: AnonymousLinksTab,
|
||||||
@@ -180,22 +185,49 @@ export const ShareSurveyModal = ({
|
|||||||
componentType: LinkSettingsTab,
|
componentType: LinkSettingsTab,
|
||||||
componentProps: { isReadOnly, locale: user.locale, isStorageConfigured },
|
componentProps: { isReadOnly, locale: user.locale, isStorageConfigured },
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
[
|
id: ShareSettingsType.PRETTY_URL,
|
||||||
t,
|
type: LinkTabsType.SHARE_SETTING,
|
||||||
survey,
|
label: t("environments.surveys.share.pretty_url.title"),
|
||||||
publicDomain,
|
icon: Link2Icon,
|
||||||
user.locale,
|
title: t("environments.surveys.share.pretty_url.title"),
|
||||||
surveyUrl,
|
description: t("environments.surveys.share.pretty_url.description"),
|
||||||
isReadOnly,
|
componentType: PrettyUrlTab,
|
||||||
environmentId,
|
componentProps: { publicDomain, isReadOnly },
|
||||||
segments,
|
},
|
||||||
isContactsEnabled,
|
{
|
||||||
isFormbricksCloud,
|
id: ShareSettingsType.CUSTOM_HTML,
|
||||||
email,
|
type: LinkTabsType.SHARE_SETTING,
|
||||||
isStorageConfigured,
|
label: t("environments.surveys.share.custom_html.nav_title"),
|
||||||
]
|
icon: CodeIcon,
|
||||||
);
|
title: t("environments.surveys.share.custom_html.nav_title"),
|
||||||
|
description: t("environments.surveys.share.custom_html.description"),
|
||||||
|
componentType: CustomHtmlTab,
|
||||||
|
componentProps: { projectCustomScripts, isReadOnly },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Filter out tabs that should not be shown on Formbricks Cloud
|
||||||
|
return isFormbricksCloud
|
||||||
|
? tabs.filter(
|
||||||
|
(tab) => tab.id !== ShareSettingsType.PRETTY_URL && tab.id !== ShareSettingsType.CUSTOM_HTML
|
||||||
|
)
|
||||||
|
: tabs;
|
||||||
|
}, [
|
||||||
|
t,
|
||||||
|
survey,
|
||||||
|
publicDomain,
|
||||||
|
user.locale,
|
||||||
|
surveyUrl,
|
||||||
|
isReadOnly,
|
||||||
|
environmentId,
|
||||||
|
segments,
|
||||||
|
isContactsEnabled,
|
||||||
|
isFormbricksCloud,
|
||||||
|
email,
|
||||||
|
isStorageConfigured,
|
||||||
|
projectCustomScripts,
|
||||||
|
]);
|
||||||
|
|
||||||
const getDefaultActiveId = useCallback(() => {
|
const getDefaultActiveId = useCallback(() => {
|
||||||
if (survey.type !== "link") {
|
if (survey.type !== "link") {
|
||||||
|
|||||||
+12
-3
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { CirclePlayIcon, CopyIcon } from "lucide-react";
|
import { CirclePlayIcon, CopyIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useMemo, 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 { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
@@ -34,7 +34,6 @@ export const AnonymousLinksTab = ({
|
|||||||
locale,
|
locale,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
}: AnonymousLinksTabProps) => {
|
}: AnonymousLinksTabProps) => {
|
||||||
const surveyUrlWithCustomSuid = `${surveyUrl}?suId=CUSTOM-ID`;
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -49,6 +48,12 @@ export const AnonymousLinksTab = ({
|
|||||||
pendingAction: () => Promise<void> | void;
|
pendingAction: () => Promise<void> | void;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
const surveyUrlWithCustomSuid = useMemo(() => {
|
||||||
|
const url = new URL(surveyUrl);
|
||||||
|
url.searchParams.set("suId", "CUSTOM-ID");
|
||||||
|
return url.toString();
|
||||||
|
}, [surveyUrl]);
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
const { singleUse } = survey;
|
const { singleUse } = survey;
|
||||||
const { enabled, isEncrypted } = singleUse ?? {};
|
const { enabled, isEncrypted } = singleUse ?? {};
|
||||||
@@ -177,7 +182,11 @@ export const AnonymousLinksTab = ({
|
|||||||
|
|
||||||
if (!!response?.data?.length) {
|
if (!!response?.data?.length) {
|
||||||
const singleUseIds = response.data;
|
const singleUseIds = response.data;
|
||||||
const surveyLinks = singleUseIds.map((singleUseId) => `${surveyUrl}?suId=${singleUseId}`);
|
const surveyLinks = singleUseIds.map((singleUseId) => {
|
||||||
|
const url = new URL(surveyUrl);
|
||||||
|
url.searchParams.set("suId", singleUseId);
|
||||||
|
return url.toString();
|
||||||
|
});
|
||||||
|
|
||||||
// Create content with just the links
|
// Create content with just the links
|
||||||
const csvContent = surveyLinks.join("\n");
|
const csvContent = surveyLinks.join("\n");
|
||||||
|
|||||||
+1
-1
@@ -163,7 +163,7 @@ export const AppTab = () => {
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
{!environment.appSetupCompleted && (
|
{!environment.appSetupCompleted && (
|
||||||
<AlertButton asChild>
|
<AlertButton asChild>
|
||||||
<Link href={`/environments/${environment.id}/project/app-connection`}>
|
<Link href={`/environments/${environment.id}/workspace/app-connection`}>
|
||||||
{t("common.connect_formbricks")}
|
{t("common.connect_formbricks")}
|
||||||
</Link>
|
</Link>
|
||||||
</AlertButton>
|
</AlertButton>
|
||||||
|
|||||||
+163
@@ -0,0 +1,163 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AlertTriangleIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { useSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
import { updateSurveyAction } from "@/modules/survey/editor/actions";
|
||||||
|
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||||
|
import { Button } from "@/modules/ui/components/button";
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormProvider,
|
||||||
|
} from "@/modules/ui/components/form";
|
||||||
|
import { TabToggle } from "@/modules/ui/components/tab-toggle";
|
||||||
|
|
||||||
|
interface CustomHtmlTabProps {
|
||||||
|
projectCustomScripts: string | null | undefined;
|
||||||
|
isReadOnly: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomHtmlFormData {
|
||||||
|
customHeadScripts: string;
|
||||||
|
customHeadScriptsMode: TSurvey["customHeadScriptsMode"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTabProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { survey } = useSurvey();
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<CustomHtmlFormData>({
|
||||||
|
defaultValues: {
|
||||||
|
customHeadScripts: survey.customHeadScripts ?? "",
|
||||||
|
customHeadScriptsMode: survey.customHeadScriptsMode ?? "add",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
reset,
|
||||||
|
formState: { isDirty },
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
const scriptsMode = watch("customHeadScriptsMode");
|
||||||
|
|
||||||
|
const onSubmit = async (data: CustomHtmlFormData) => {
|
||||||
|
if (isSaving || isReadOnly) return;
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
const updatedSurvey: TSurvey = {
|
||||||
|
...survey,
|
||||||
|
customHeadScripts: data.customHeadScripts || null,
|
||||||
|
customHeadScriptsMode: data.customHeadScriptsMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await updateSurveyAction(updatedSurvey);
|
||||||
|
|
||||||
|
if (result?.data) {
|
||||||
|
toast.success(t("environments.surveys.share.custom_html.saved_successfully"));
|
||||||
|
reset(data);
|
||||||
|
} else {
|
||||||
|
toast.error(t("common.something_went_wrong_please_try_again"));
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-1">
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
{/* Mode Toggle */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>{t("environments.surveys.share.custom_html.script_mode")}</FormLabel>
|
||||||
|
<TabToggle
|
||||||
|
id="custom-scripts-mode"
|
||||||
|
options={[
|
||||||
|
{ value: "add", label: t("environments.surveys.share.custom_html.add_to_workspace") },
|
||||||
|
{ value: "replace", label: t("environments.surveys.share.custom_html.replace_workspace") },
|
||||||
|
]}
|
||||||
|
defaultSelected={scriptsMode ?? "add"}
|
||||||
|
onChange={(value) => setValue("customHeadScriptsMode", value, { shouldDirty: true })}
|
||||||
|
disabled={isReadOnly}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
|
{scriptsMode === "add"
|
||||||
|
? t("environments.surveys.share.custom_html.add_mode_description")
|
||||||
|
: t("environments.surveys.share.custom_html.replace_mode_description")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Workspace Scripts Preview */}
|
||||||
|
{projectCustomScripts && (
|
||||||
|
<div className={scriptsMode === "replace" ? "opacity-50" : ""}>
|
||||||
|
<FormLabel>{t("environments.surveys.share.custom_html.workspace_scripts_label")}</FormLabel>
|
||||||
|
<div className="mt-2 max-h-32 overflow-auto rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||||
|
<pre className="font-mono text-xs whitespace-pre-wrap text-slate-600">
|
||||||
|
{projectCustomScripts}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!projectCustomScripts && (
|
||||||
|
<div className="rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
|
{t("environments.surveys.share.custom_html.no_workspace_scripts")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Survey Scripts */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="customHeadScripts"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("environments.surveys.share.custom_html.survey_scripts_label")}</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t("environments.surveys.share.custom_html.survey_scripts_description")}
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<textarea
|
||||||
|
rows={8}
|
||||||
|
placeholder={t("environments.surveys.share.custom_html.placeholder")}
|
||||||
|
className={cn(
|
||||||
|
"focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
)}
|
||||||
|
{...field}
|
||||||
|
disabled={isReadOnly}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<Button type="submit" disabled={isSaving || isReadOnly || !isDirty}>
|
||||||
|
{isSaving ? t("common.saving") : t("common.save")}
|
||||||
|
</Button>
|
||||||
|
{/* Security Warning */}
|
||||||
|
<Alert variant="warning" className="flex items-start gap-2">
|
||||||
|
<AlertTriangleIcon className="mt-0.5 h-4 w-4 shrink-0" />
|
||||||
|
<AlertDescription>
|
||||||
|
{t("environments.surveys.share.custom_html.security_warning")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Input } from "@/modules/ui/components/input";
|
||||||
|
|
||||||
|
interface PrettyUrlInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
publicDomain: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrettyUrlInput = ({ value, onChange, publicDomain, disabled = false }: PrettyUrlInputProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center overflow-hidden rounded-md border border-slate-300 bg-white">
|
||||||
|
<span className="flex-shrink-0 border-r border-slate-300 bg-slate-50 px-3 py-2 text-sm text-slate-600">
|
||||||
|
{publicDomain}/p/
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value.toLowerCase().replaceAll(/[^a-z0-9-]/g, ""))}
|
||||||
|
placeholder={t("environments.surveys.share.pretty_url.slug_placeholder")}
|
||||||
|
disabled={disabled}
|
||||||
|
className="border-0 bg-white focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Copy, Trash2 } from "lucide-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context";
|
||||||
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
|
import { removeSurveySlugAction, updateSurveySlugAction } from "@/modules/survey/slug/actions";
|
||||||
|
import { Button } from "@/modules/ui/components/button";
|
||||||
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormProvider,
|
||||||
|
} from "@/modules/ui/components/form";
|
||||||
|
import { PrettyUrlInput } from "./pretty-url-input";
|
||||||
|
|
||||||
|
interface PrettyUrlTabProps {
|
||||||
|
publicDomain: string;
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PrettyUrlFormData {
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrettyUrlTab = ({ publicDomain, isReadOnly = false }: PrettyUrlTabProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
const { survey } = useSurvey();
|
||||||
|
const [isEditing, setIsEditing] = useState(!survey.slug);
|
||||||
|
const [showRemoveDialog, setShowRemoveDialog] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Initialize form with current values - memoize to prevent re-initialization
|
||||||
|
const initialFormData = useMemo(() => {
|
||||||
|
return {
|
||||||
|
slug: survey.slug || "",
|
||||||
|
};
|
||||||
|
}, [survey.slug]);
|
||||||
|
|
||||||
|
const form = useForm<PrettyUrlFormData>({
|
||||||
|
defaultValues: initialFormData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleSubmit, reset } = form;
|
||||||
|
|
||||||
|
// Sync isEditing state and form with survey.slug changes
|
||||||
|
useEffect(() => {
|
||||||
|
setIsEditing(!survey.slug);
|
||||||
|
reset({ slug: survey.slug || "" });
|
||||||
|
}, [survey.slug, reset]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: PrettyUrlFormData) => {
|
||||||
|
if (!data.slug.trim()) {
|
||||||
|
toast.error(t("environments.surveys.share.pretty_url.slug_required"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
const result = await updateSurveySlugAction({
|
||||||
|
surveyId: survey.id,
|
||||||
|
slug: data.slug,
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorMessage = getFormattedErrorMessage(result);
|
||||||
|
if (errorMessage) {
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} else {
|
||||||
|
toast.success(t("environments.surveys.share.pretty_url.save_success"));
|
||||||
|
router.refresh();
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : t("common.something_went_wrong_please_try_again");
|
||||||
|
toast.error(message);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
setIsEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
reset({ slug: survey.slug || "" });
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = async () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
const result = await removeSurveySlugAction({ surveyId: survey.id });
|
||||||
|
|
||||||
|
const errorMessage = getFormattedErrorMessage(result);
|
||||||
|
if (errorMessage) {
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} else {
|
||||||
|
setShowRemoveDialog(false);
|
||||||
|
reset({ slug: "" });
|
||||||
|
router.refresh();
|
||||||
|
setIsEditing(true);
|
||||||
|
toast.success(t("environments.surveys.share.pretty_url.remove_success"));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : t("common.something_went_wrong_please_try_again");
|
||||||
|
toast.error(message);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyUrl = () => {
|
||||||
|
if (!survey.slug) return;
|
||||||
|
const prettyUrl = `${publicDomain}/p/${survey.slug}`;
|
||||||
|
navigator.clipboard.writeText(prettyUrl);
|
||||||
|
toast.success(t("common.copied_to_clipboard"));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-1">
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="slug"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("environments.surveys.share.pretty_url.slug_label")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<PrettyUrlInput
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
publicDomain={publicDomain}
|
||||||
|
disabled={isReadOnly || !isEditing}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>{t("environments.surveys.share.pretty_url.slug_help")}</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<Button type="submit" disabled={isReadOnly || isSubmitting}>
|
||||||
|
{t("common.save")}
|
||||||
|
</Button>
|
||||||
|
{survey.slug && (
|
||||||
|
<Button type="button" variant="secondary" onClick={handleCancel} disabled={isSubmitting}>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button type="button" variant="secondary" onClick={handleEdit} disabled={isReadOnly}>
|
||||||
|
{t("common.edit")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{survey.slug && !isEditing && (
|
||||||
|
<>
|
||||||
|
<Button type="button" variant="default" onClick={handleCopyUrl} disabled={isReadOnly}>
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
{t("common.copy")} URL
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => setShowRemoveDialog(true)}
|
||||||
|
disabled={isReadOnly}>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
{t("common.remove")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
|
||||||
|
<DeleteDialog
|
||||||
|
open={showRemoveDialog}
|
||||||
|
setOpen={setShowRemoveDialog}
|
||||||
|
deleteWhat={t("environments.surveys.share.pretty_url.title")}
|
||||||
|
onDelete={handleRemove}
|
||||||
|
text={t("environments.surveys.share.pretty_url.remove_description")}></DeleteDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+2
-2
@@ -66,7 +66,7 @@ export const SuccessView: React.FC<SuccessViewProps> = ({
|
|||||||
className="relative flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-center text-sm text-slate-900 hover:border-slate-200 md:p-8">
|
className="relative flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-center text-sm text-slate-900 hover:border-slate-200 md:p-8">
|
||||||
<UserIcon className="h-8 w-8 stroke-1 text-slate-900" />
|
<UserIcon className="h-8 w-8 stroke-1 text-slate-900" />
|
||||||
{t("environments.surveys.summary.use_personal_links")}
|
{t("environments.surveys.summary.use_personal_links")}
|
||||||
<Badge size="normal" type="success" className="absolute right-3 top-3" text={t("common.new")} />
|
<Badge size="normal" type="success" className="absolute top-3 right-3" text={t("common.new")} />
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
href={`/environments/${environmentId}/settings/notifications`}
|
href={`/environments/${environmentId}/settings/notifications`}
|
||||||
@@ -75,7 +75,7 @@ export const SuccessView: React.FC<SuccessViewProps> = ({
|
|||||||
{t("environments.surveys.summary.configure_alerts")}
|
{t("environments.surveys.summary.configure_alerts")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={`/environments/${environmentId}/project/integrations`}
|
href={`/environments/${environmentId}/workspace/integrations`}
|
||||||
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-center text-sm text-slate-900 hover:border-slate-200 md:p-8">
|
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-center text-sm text-slate-900 hover:border-slate-200 md:p-8">
|
||||||
<BlocksIcon className="h-8 w-8 stroke-1 text-slate-900" />
|
<BlocksIcon className="h-8 w-8 stroke-1 text-slate-900" />
|
||||||
{t("environments.surveys.summary.setup_integrations")}
|
{t("environments.surveys.summary.setup_integrations")}
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ export const getEmailTemplateHtml = async (surveyId: string, locale: string) =>
|
|||||||
}
|
}
|
||||||
const project = await getProjectByEnvironmentId(survey.environmentId);
|
const project = await getProjectByEnvironmentId(survey.environmentId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error("Project not found");
|
throw new Error("Workspace not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const styling = getStyling(project, survey);
|
const styling = getStyling(project, survey);
|
||||||
|
|||||||
+2
@@ -12,6 +12,8 @@ export enum ShareViaType {
|
|||||||
|
|
||||||
export enum ShareSettingsType {
|
export enum ShareSettingsType {
|
||||||
LINK_SETTINGS = "link-settings",
|
LINK_SETTINGS = "link-settings",
|
||||||
|
PRETTY_URL = "pretty-url",
|
||||||
|
CUSTOM_HTML = "custom-html",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LinkTabsType {
|
export enum LinkTabsType {
|
||||||
|
|||||||
+3
-1
@@ -21,6 +21,7 @@ import {
|
|||||||
ListOrderedIcon,
|
ListOrderedIcon,
|
||||||
MessageSquareTextIcon,
|
MessageSquareTextIcon,
|
||||||
MousePointerClickIcon,
|
MousePointerClickIcon,
|
||||||
|
NetworkIcon,
|
||||||
PieChartIcon,
|
PieChartIcon,
|
||||||
Rows3Icon,
|
Rows3Icon,
|
||||||
SmartphoneIcon,
|
SmartphoneIcon,
|
||||||
@@ -99,6 +100,7 @@ const elementIcons = {
|
|||||||
action: MousePointerClickIcon,
|
action: MousePointerClickIcon,
|
||||||
country: FlagIcon,
|
country: FlagIcon,
|
||||||
url: LinkIcon,
|
url: LinkIcon,
|
||||||
|
ipAddress: NetworkIcon,
|
||||||
|
|
||||||
// others
|
// others
|
||||||
Language: LanguagesIcon,
|
Language: LanguagesIcon,
|
||||||
@@ -190,7 +192,7 @@ export const ElementsComboBox = ({ options, selected, onChangeValue }: ElementCo
|
|||||||
value={inputValue}
|
value={inputValue}
|
||||||
onValueChange={setInputValue}
|
onValueChange={setInputValue}
|
||||||
placeholder={open ? `${t("common.search")}...` : t("common.select_filter")}
|
placeholder={open ? `${t("common.search")}...` : t("common.select_filter")}
|
||||||
className="max-w-full grow border-none p-0 pl-2 text-sm shadow-none outline-none ring-offset-transparent focus:border-none focus:shadow-none focus:outline-none focus:ring-offset-0"
|
className="max-w-full grow border-none p-0 pl-2 text-sm shadow-none ring-offset-transparent outline-none focus:border-none focus:shadow-none focus:ring-offset-0 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
+10
-10
@@ -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]/project/integrations/actions";
|
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
||||||
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/BaseSelectDropdown";
|
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/BaseSelectDropdown";
|
||||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable";
|
import { fetchTables } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/lib/airtable";
|
||||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||||
import { recallToHeadline } from "@/lib/utils/recall";
|
import { recallToHeadline } from "@/lib/utils/recall";
|
||||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||||
@@ -58,12 +58,12 @@ const ElementCheckbox = ({
|
|||||||
onChange: (value: string[]) => void;
|
onChange: (value: string[]) => void;
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const handleCheckedChange = (checked: boolean) => {
|
const addElement = () => {
|
||||||
if (checked) {
|
field.onChange([...(field.value || []), element.id]);
|
||||||
field.onChange([...(field.value || []), element.id]);
|
};
|
||||||
} else {
|
|
||||||
field.onChange(field.value?.filter((value) => value !== element.id) || []);
|
const removeElement = () => {
|
||||||
}
|
field.onChange(field.value?.filter((value) => value !== element.id) || []);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -75,7 +75,7 @@ const ElementCheckbox = ({
|
|||||||
value={element.id}
|
value={element.id}
|
||||||
className="bg-white"
|
className="bg-white"
|
||||||
checked={field.value?.includes(element.id)}
|
checked={field.value?.includes(element.id)}
|
||||||
onCheckedChange={handleCheckedChange}
|
onCheckedChange={(checked) => (checked ? addElement() : removeElement())}
|
||||||
/>
|
/>
|
||||||
<span className="ml-2">
|
<span className="ml-2">
|
||||||
{getTextContent(recallToHeadline(element.headline, selectedSurvey, false, "default")["default"])}
|
{getTextContent(recallToHeadline(element.headline, selectedSurvey, false, "default")["default"])}
|
||||||
+2
-2
@@ -5,8 +5,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 { ManageIntegration } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/ManageIntegration";
|
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/ManageIntegration";
|
||||||
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/lib/airtable";
|
import { authorize } from "@/app/(app)/environments/[environmentId]/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";
|
||||||
|
|
||||||
+2
-2
@@ -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]/project/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
||||||
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/project/integrations/airtable/components/AddIntegrationModal";
|
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/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";
|
||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
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]/project/integrations/airtable/components/AirtableWrapper";
|
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/AirtableWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
||||||
import { getAirtableTables } from "@/lib/airtable/service";
|
import { getAirtableTables } from "@/lib/airtable/service";
|
||||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@/lib/constants";
|
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@/lib/constants";
|
||||||
import { getIntegrations } from "@/lib/integration/service";
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
@@ -42,7 +42,7 @@ const Page = async (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/project/integrations`} />
|
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/workspace/integrations`} />
|
||||||
<PageHeader pageTitle={t("environments.integrations.airtable.airtable_integration")} />
|
<PageHeader pageTitle={t("environments.integrations.airtable.airtable_integration")} />
|
||||||
<div className="h-[75vh] w-full">
|
<div className="h-[75vh] w-full">
|
||||||
<AirtableWrapper
|
<AirtableWrapper
|
||||||
+4
-4
@@ -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]/project/integrations/actions";
|
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
||||||
import { getSpreadsheetNameByIdAction } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/actions";
|
import { getSpreadsheetNameByIdAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/actions";
|
||||||
import {
|
import {
|
||||||
constructGoogleSheetsUrl,
|
constructGoogleSheetsUrl,
|
||||||
extractSpreadsheetIdFromUrl,
|
extractSpreadsheetIdFromUrl,
|
||||||
isValidGoogleSheetsUrl,
|
isValidGoogleSheetsUrl,
|
||||||
} from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/util";
|
} from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/lib/util";
|
||||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
import GoogleSheetLogo from "@/images/googleSheetsLogo.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";
|
||||||
@@ -266,7 +266,7 @@ export const AddIntegrationModal = ({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
||||||
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden rounded-lg border border-slate-200">
|
<div className="mt-1 max-h-[15vh] overflow-x-hidden overflow-y-auto rounded-lg border border-slate-200">
|
||||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||||
{surveyElements.map((question) => (
|
{surveyElements.map((question) => (
|
||||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||||
+2
-2
@@ -8,8 +8,8 @@ import {
|
|||||||
} 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 { ManageIntegration } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/components/ManageIntegration";
|
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/ManageIntegration";
|
||||||
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/google-sheets/lib/google";
|
import { authorize } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/lib/google";
|
||||||
import googleSheetLogo from "@/images/googleSheetsLogo.png";
|
import googleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||||
import { AddIntegrationModal } from "./AddIntegrationModal";
|
import { AddIntegrationModal } from "./AddIntegrationModal";
|
||||||
+1
-1
@@ -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]/project/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/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";
|
||||||
+2
-2
@@ -10,7 +10,7 @@ const Loading = () => {
|
|||||||
<div className="mt-6 p-6">
|
<div className="mt-6 p-6">
|
||||||
<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 bg-slate-200 select-none">
|
||||||
{t("environments.integrations.google_sheets.link_new_sheet")}
|
{t("environments.integrations.google_sheets.link_new_sheet")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,7 +51,7 @@ const Loading = () => {
|
|||||||
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
|
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm whitespace-nowrap text-slate-500">
|
||||||
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
|
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center"></div>
|
<div className="text-center"></div>
|
||||||
+3
-3
@@ -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]/project/integrations/google-sheets/components/GoogleSheetWrapper";
|
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
||||||
import {
|
import {
|
||||||
GOOGLE_SHEETS_CLIENT_ID,
|
GOOGLE_SHEETS_CLIENT_ID,
|
||||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||||
@@ -40,7 +40,7 @@ const Page = async (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/project/integrations`} />
|
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/workspace/integrations`} />
|
||||||
<PageHeader pageTitle={t("environments.integrations.google_sheets.google_sheets_integration")} />
|
<PageHeader pageTitle={t("environments.integrations.google_sheets.google_sheets_integration")} />
|
||||||
<div className="h-[75vh] w-full">
|
<div className="h-[75vh] w-full">
|
||||||
<GoogleSheetWrapper
|
<GoogleSheetWrapper
|
||||||
+24
-227
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
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";
|
||||||
@@ -15,17 +15,15 @@ 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]/project/integrations/actions";
|
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
||||||
import {
|
import {
|
||||||
ERRORS,
|
MappingRow,
|
||||||
TYPE_MAPPING,
|
TMapping,
|
||||||
UNSUPPORTED_TYPES_BY_NOTION,
|
createEmptyMapping,
|
||||||
} from "@/app/(app)/environments/[environmentId]/project/integrations/notion/constants";
|
} from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/MappingRow";
|
||||||
import NotionLogo from "@/images/notion.png";
|
import NotionLogo from "@/images/notion.png";
|
||||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
|
||||||
import { recallToHeadline } from "@/lib/utils/recall";
|
import { recallToHeadline } from "@/lib/utils/recall";
|
||||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||||
import { getElementTypes } from "@/modules/survey/lib/elements";
|
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -39,59 +37,6 @@ import {
|
|||||||
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
|
|
||||||
const MappingErrorMessage = ({
|
|
||||||
error,
|
|
||||||
col,
|
|
||||||
elem,
|
|
||||||
t,
|
|
||||||
}: {
|
|
||||||
error: { type: string; msg?: React.ReactNode | string } | null | undefined;
|
|
||||||
col: { id: string; name: string; type: string };
|
|
||||||
elem: { id: string; name: string; type: string };
|
|
||||||
t: ReturnType<typeof useTranslation>["t"];
|
|
||||||
}) => {
|
|
||||||
const showErrorMsg = useMemo(() => {
|
|
||||||
switch (error?.type) {
|
|
||||||
case ERRORS.UNSUPPORTED_TYPE:
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
-{" "}
|
|
||||||
{t("environments.integrations.notion.col_name_of_type_is_not_supported", {
|
|
||||||
col_name: col.name,
|
|
||||||
type: col.type,
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case ERRORS.MAPPING:
|
|
||||||
const element = getElementTypes(t).find((et) => et.id === elem.type);
|
|
||||||
if (!element) return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{t("environments.integrations.notion.que_name_of_type_cant_be_mapped_to", {
|
|
||||||
que_name: elem.name,
|
|
||||||
question_label: element.label,
|
|
||||||
col_name: col.name,
|
|
||||||
col_type: col.type,
|
|
||||||
mapped_type: TYPE_MAPPING[element.id].join(" ,"),
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [error, col, elem, t]);
|
|
||||||
|
|
||||||
if (!error) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="my-4 w-full rounded-lg bg-red-100 p-4 text-sm text-red-800">
|
|
||||||
<span className="mb-2 block">{error.type}</span>
|
|
||||||
{showErrorMsg}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface AddIntegrationModalProps {
|
interface AddIntegrationModalProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
surveys: TSurvey[];
|
surveys: TSurvey[];
|
||||||
@@ -115,21 +60,7 @@ export const AddIntegrationModal = ({
|
|||||||
const { handleSubmit } = useForm();
|
const { handleSubmit } = useForm();
|
||||||
const [selectedDatabase, setSelectedDatabase] = useState<TIntegrationNotionDatabase | null>();
|
const [selectedDatabase, setSelectedDatabase] = useState<TIntegrationNotionDatabase | null>();
|
||||||
const [selectedSurvey, setSelectedSurvey] = useState<TSurvey | null>(null);
|
const [selectedSurvey, setSelectedSurvey] = useState<TSurvey | null>(null);
|
||||||
const [mapping, setMapping] = useState<
|
const [mapping, setMapping] = useState<TMapping[]>([createEmptyMapping()]);
|
||||||
{
|
|
||||||
column: { id: string; name: string; type: string };
|
|
||||||
element: { id: string; name: string; type: string };
|
|
||||||
error?: {
|
|
||||||
type: string;
|
|
||||||
msg: React.ReactNode | string;
|
|
||||||
} | null;
|
|
||||||
}[]
|
|
||||||
>([
|
|
||||||
{
|
|
||||||
column: { id: "", name: "", type: "" },
|
|
||||||
element: { id: "", name: "", type: "" },
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||||
const [isLinkingDatabase, setIsLinkingDatabase] = useState(false);
|
const [isLinkingDatabase, setIsLinkingDatabase] = useState(false);
|
||||||
const integrationData = {
|
const integrationData = {
|
||||||
@@ -234,7 +165,7 @@ export const AddIntegrationModal = ({
|
|||||||
return survey.id === selectedIntegration.surveyId;
|
return survey.id === selectedIntegration.surveyId;
|
||||||
})!
|
})!
|
||||||
);
|
);
|
||||||
setMapping(selectedIntegration.mapping);
|
setMapping(selectedIntegration.mapping.map((m) => ({ ...m, id: createId() })));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resetForm();
|
resetForm();
|
||||||
@@ -320,154 +251,11 @@ export const AddIntegrationModal = ({
|
|||||||
setSelectedDatabase(null);
|
setSelectedDatabase(null);
|
||||||
setSelectedSurvey(null);
|
setSelectedSurvey(null);
|
||||||
};
|
};
|
||||||
const getFilteredElementItems = (selectedIdx) => {
|
const getFilteredElementItems = (selectedIdx: number) => {
|
||||||
const selectedElementIds = mapping.filter((_, idx) => idx !== selectedIdx).map((m) => m.element.id);
|
const selectedElementIds = new Set(
|
||||||
|
mapping.filter((_, idx) => idx !== selectedIdx).map((m) => m.element.id)
|
||||||
return elementItems.filter((el) => !selectedElementIds.includes(el.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const createCopy = (item) => structuredClone(item);
|
|
||||||
|
|
||||||
const MappingRow = ({ idx }: { idx: number }) => {
|
|
||||||
const filteredElementItems = getFilteredElementItems(idx);
|
|
||||||
|
|
||||||
const addRow = () => {
|
|
||||||
setMapping((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
column: { id: "", name: "", type: "" },
|
|
||||||
element: { id: "", name: "", type: "" },
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteRow = () => {
|
|
||||||
setMapping((prev) => {
|
|
||||||
return prev.filter((_, i) => i !== idx);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilteredDbItems = () => {
|
|
||||||
const colMapping = mapping.map((m) => m.column.id);
|
|
||||||
return dbItems.filter((item) => !colMapping.includes(item.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<MappingErrorMessage
|
|
||||||
key={idx}
|
|
||||||
error={mapping[idx]?.error}
|
|
||||||
col={mapping[idx].column}
|
|
||||||
elem={mapping[idx].element}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
<div className="flex w-full items-center space-x-2">
|
|
||||||
<div className="flex w-full items-center">
|
|
||||||
<div className="max-w-full flex-1">
|
|
||||||
<DropdownSelector
|
|
||||||
placeholder={t("environments.integrations.notion.select_a_survey_question")}
|
|
||||||
items={filteredElementItems}
|
|
||||||
selectedItem={mapping?.[idx]?.element}
|
|
||||||
setSelectedItem={(item) => {
|
|
||||||
setMapping((prev) => {
|
|
||||||
const copy = createCopy(prev);
|
|
||||||
const col = copy[idx].column;
|
|
||||||
if (col.id) {
|
|
||||||
if (UNSUPPORTED_TYPES_BY_NOTION.includes(col.type)) {
|
|
||||||
copy[idx] = {
|
|
||||||
...copy[idx],
|
|
||||||
error: {
|
|
||||||
type: ERRORS.UNSUPPORTED_TYPE,
|
|
||||||
},
|
|
||||||
element: item,
|
|
||||||
};
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValidColType = TYPE_MAPPING[item.type].includes(col.type);
|
|
||||||
if (!isValidColType) {
|
|
||||||
copy[idx] = {
|
|
||||||
...copy[idx],
|
|
||||||
error: {
|
|
||||||
type: ERRORS.MAPPING,
|
|
||||||
},
|
|
||||||
element: item,
|
|
||||||
};
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
copy[idx] = {
|
|
||||||
...copy[idx],
|
|
||||||
element: item,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
return copy;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disabled={elementItems.length === 0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="h-px w-4 border-t border-t-slate-300" />
|
|
||||||
<div className="max-w-full flex-1">
|
|
||||||
<DropdownSelector
|
|
||||||
placeholder={t("environments.integrations.notion.select_a_field_to_map")}
|
|
||||||
items={getFilteredDbItems()}
|
|
||||||
selectedItem={mapping?.[idx]?.column}
|
|
||||||
setSelectedItem={(item) => {
|
|
||||||
setMapping((prev) => {
|
|
||||||
const copy = createCopy(prev);
|
|
||||||
const elem = copy[idx].element;
|
|
||||||
if (elem.id) {
|
|
||||||
const isValidElemType = TYPE_MAPPING[elem.type].includes(item.type);
|
|
||||||
|
|
||||||
if (UNSUPPORTED_TYPES_BY_NOTION.includes(item.type)) {
|
|
||||||
copy[idx] = {
|
|
||||||
...copy[idx],
|
|
||||||
error: {
|
|
||||||
type: ERRORS.UNSUPPORTED_TYPE,
|
|
||||||
},
|
|
||||||
column: item,
|
|
||||||
};
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValidElemType) {
|
|
||||||
copy[idx] = {
|
|
||||||
...copy[idx],
|
|
||||||
error: {
|
|
||||||
type: ERRORS.MAPPING,
|
|
||||||
},
|
|
||||||
column: item,
|
|
||||||
};
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
copy[idx] = {
|
|
||||||
...copy[idx],
|
|
||||||
column: item,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
return copy;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disabled={dbItems.length === 0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
{mapping.length > 1 && (
|
|
||||||
<Button variant="secondary" size="icon" className="size-10" onClick={deleteRow}>
|
|
||||||
<TrashIcon />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button variant="secondary" size="icon" className="size-10" onClick={addRow}>
|
|
||||||
<PlusIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
return elementItems.filter((el) => !selectedElementIds.has(el.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -539,8 +327,17 @@ export const AddIntegrationModal = ({
|
|||||||
{t("environments.integrations.notion.map_formbricks_fields_to_notion_property")}
|
{t("environments.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((_, idx) => (
|
{mapping.map((m, idx) => (
|
||||||
<MappingRow idx={idx} key={idx} />
|
<MappingRow
|
||||||
|
key={m.id}
|
||||||
|
idx={idx}
|
||||||
|
mapping={mapping}
|
||||||
|
setMapping={setMapping}
|
||||||
|
filteredElementItems={getFilteredElementItems(idx)}
|
||||||
|
dbItems={dbItems}
|
||||||
|
elementItems={elementItems}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
+1
-1
@@ -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]/project/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/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";
|
||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
ERRORS,
|
||||||
|
TYPE_MAPPING,
|
||||||
|
UNSUPPORTED_TYPES_BY_NOTION,
|
||||||
|
} from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/constants";
|
||||||
|
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||||
|
import { getElementTypes } from "@/modules/survey/lib/elements";
|
||||||
|
import { Button } from "@/modules/ui/components/button";
|
||||||
|
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
||||||
|
|
||||||
|
const filterByIdx = (targetIdx: number) => (_: unknown, i: number) => i !== targetIdx;
|
||||||
|
|
||||||
|
export type TColumnOrElement = { id: string; name: string; type: string };
|
||||||
|
export type TMappingError = { type: string; msg?: React.ReactNode | string } | null;
|
||||||
|
export type TMapping = {
|
||||||
|
id: string;
|
||||||
|
column: TColumnOrElement;
|
||||||
|
element: TColumnOrElement;
|
||||||
|
error?: TMappingError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createEmptyMapping = (): TMapping => ({
|
||||||
|
id: createId(),
|
||||||
|
column: { id: "", name: "", type: "" },
|
||||||
|
element: { id: "", name: "", type: "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const MappingErrorMessage = ({
|
||||||
|
error,
|
||||||
|
col,
|
||||||
|
elem,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
error: TMappingError | undefined;
|
||||||
|
col: TColumnOrElement;
|
||||||
|
elem: TColumnOrElement;
|
||||||
|
t: ReturnType<typeof useTranslation>["t"];
|
||||||
|
}) => {
|
||||||
|
const showErrorMsg = useMemo(() => {
|
||||||
|
switch (error?.type) {
|
||||||
|
case ERRORS.UNSUPPORTED_TYPE: {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
-{" "}
|
||||||
|
{t("environments.integrations.notion.col_name_of_type_is_not_supported", {
|
||||||
|
col_name: col.name,
|
||||||
|
type: col.type,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case ERRORS.MAPPING: {
|
||||||
|
const element = getElementTypes(t).find((et) => et.id === elem.type);
|
||||||
|
if (!element) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{t("environments.integrations.notion.que_name_of_type_cant_be_mapped_to", {
|
||||||
|
que_name: elem.name,
|
||||||
|
question_label: element.label,
|
||||||
|
col_name: col.name,
|
||||||
|
col_type: col.type,
|
||||||
|
mapped_type: TYPE_MAPPING[element.id].join(" ,"),
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [error, col, elem, t]);
|
||||||
|
|
||||||
|
if (!error) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="my-4 w-full rounded-lg bg-red-100 p-4 text-sm text-red-800">
|
||||||
|
<span className="mb-2 block">{error.type}</span>
|
||||||
|
{showErrorMsg}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MappingRowProps {
|
||||||
|
idx: number;
|
||||||
|
mapping: TMapping[];
|
||||||
|
setMapping: React.Dispatch<React.SetStateAction<TMapping[]>>;
|
||||||
|
filteredElementItems: TColumnOrElement[];
|
||||||
|
dbItems: TColumnOrElement[];
|
||||||
|
elementItems: TColumnOrElement[];
|
||||||
|
t: ReturnType<typeof useTranslation>["t"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MappingRow = ({
|
||||||
|
idx,
|
||||||
|
mapping,
|
||||||
|
setMapping,
|
||||||
|
filteredElementItems,
|
||||||
|
dbItems,
|
||||||
|
elementItems,
|
||||||
|
t,
|
||||||
|
}: MappingRowProps) => {
|
||||||
|
const createCopy = (items: TMapping[]) => structuredClone(items);
|
||||||
|
|
||||||
|
const addRow = () => {
|
||||||
|
setMapping((prev) => [...prev, createEmptyMapping()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteRow = () => {
|
||||||
|
setMapping((prev) => prev.filter(filterByIdx(idx)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilteredDbItems = () => {
|
||||||
|
const colMapping = new Set(mapping.map((m) => m.column.id));
|
||||||
|
return dbItems.filter((item) => !colMapping.has(item.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleElementSelect = (item: TColumnOrElement) => {
|
||||||
|
setMapping((prev) => {
|
||||||
|
const copy = createCopy(prev);
|
||||||
|
const col = copy[idx].column;
|
||||||
|
|
||||||
|
if (col.id) {
|
||||||
|
if (UNSUPPORTED_TYPES_BY_NOTION.includes(col.type)) {
|
||||||
|
copy[idx] = {
|
||||||
|
...copy[idx],
|
||||||
|
error: { type: ERRORS.UNSUPPORTED_TYPE },
|
||||||
|
element: item,
|
||||||
|
};
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidColType = TYPE_MAPPING[item.type].includes(col.type);
|
||||||
|
if (!isValidColType) {
|
||||||
|
copy[idx] = {
|
||||||
|
...copy[idx],
|
||||||
|
error: { type: ERRORS.MAPPING },
|
||||||
|
element: item,
|
||||||
|
};
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copy[idx] = { ...copy[idx], element: item, error: null };
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnSelect = (item: TColumnOrElement) => {
|
||||||
|
setMapping((prev) => {
|
||||||
|
const copy = createCopy(prev);
|
||||||
|
const elem = copy[idx].element;
|
||||||
|
|
||||||
|
if (elem.id) {
|
||||||
|
if (UNSUPPORTED_TYPES_BY_NOTION.includes(item.type)) {
|
||||||
|
copy[idx] = {
|
||||||
|
...copy[idx],
|
||||||
|
error: { type: ERRORS.UNSUPPORTED_TYPE },
|
||||||
|
column: item,
|
||||||
|
};
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidElemType = TYPE_MAPPING[elem.type].includes(item.type);
|
||||||
|
if (!isValidElemType) {
|
||||||
|
copy[idx] = {
|
||||||
|
...copy[idx],
|
||||||
|
error: { type: ERRORS.MAPPING },
|
||||||
|
column: item,
|
||||||
|
};
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copy[idx] = { ...copy[idx], column: item, error: null };
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<MappingErrorMessage
|
||||||
|
error={mapping[idx]?.error}
|
||||||
|
col={mapping[idx].column}
|
||||||
|
elem={mapping[idx].element}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
<div className="flex w-full items-center space-x-2">
|
||||||
|
<div className="flex w-full items-center">
|
||||||
|
<div className="max-w-full flex-1">
|
||||||
|
<DropdownSelector
|
||||||
|
placeholder={t("environments.integrations.notion.select_a_survey_question")}
|
||||||
|
items={filteredElementItems}
|
||||||
|
selectedItem={mapping?.[idx]?.element}
|
||||||
|
setSelectedItem={handleElementSelect}
|
||||||
|
disabled={elementItems.length === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="h-px w-4 border-t border-t-slate-300" />
|
||||||
|
<div className="max-w-full flex-1">
|
||||||
|
<DropdownSelector
|
||||||
|
placeholder={t("environments.integrations.notion.select_a_field_to_map")}
|
||||||
|
items={getFilteredDbItems()}
|
||||||
|
selectedItem={mapping?.[idx]?.column}
|
||||||
|
setSelectedItem={handleColumnSelect}
|
||||||
|
disabled={dbItems.length === 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
{mapping.length > 1 && (
|
||||||
|
<Button variant="secondary" size="icon" className="size-10" onClick={deleteRow} type="button">
|
||||||
|
<TrashIcon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="secondary" size="icon" className="size-10" onClick={addRow} type="button">
|
||||||
|
<PlusIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+2
-2
@@ -9,8 +9,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]/project/integrations/notion/components/AddIntegrationModal";
|
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/AddIntegrationModal";
|
||||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/project/integrations/notion/components/ManageIntegration";
|
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/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";
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||||
|
|
||||||
|
export const TYPE_MAPPING = {
|
||||||
|
[TSurveyElementTypeEnum.CTA]: ["checkbox"],
|
||||||
|
[TSurveyElementTypeEnum.MultipleChoiceMulti]: ["multi_select"],
|
||||||
|
[TSurveyElementTypeEnum.MultipleChoiceSingle]: ["select", "status"],
|
||||||
|
[TSurveyElementTypeEnum.OpenText]: [
|
||||||
|
"created_by",
|
||||||
|
"created_time",
|
||||||
|
"email",
|
||||||
|
"last_edited_by",
|
||||||
|
"last_edited_time",
|
||||||
|
"number",
|
||||||
|
"phone_number",
|
||||||
|
"rich_text",
|
||||||
|
"title",
|
||||||
|
"url",
|
||||||
|
],
|
||||||
|
[TSurveyElementTypeEnum.NPS]: ["number"],
|
||||||
|
[TSurveyElementTypeEnum.Consent]: ["checkbox"],
|
||||||
|
[TSurveyElementTypeEnum.Rating]: ["number"],
|
||||||
|
[TSurveyElementTypeEnum.PictureSelection]: ["url"],
|
||||||
|
[TSurveyElementTypeEnum.FileUpload]: ["url"],
|
||||||
|
[TSurveyElementTypeEnum.Date]: ["date"],
|
||||||
|
[TSurveyElementTypeEnum.Address]: ["rich_text"],
|
||||||
|
[TSurveyElementTypeEnum.Matrix]: ["rich_text"],
|
||||||
|
[TSurveyElementTypeEnum.Cal]: ["checkbox"],
|
||||||
|
[TSurveyElementTypeEnum.ContactInfo]: ["rich_text"],
|
||||||
|
[TSurveyElementTypeEnum.Ranking]: ["rich_text"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UNSUPPORTED_TYPES_BY_NOTION = [
|
||||||
|
"rollup",
|
||||||
|
"created_by",
|
||||||
|
"created_time",
|
||||||
|
"last_edited_by",
|
||||||
|
"last_edited_time",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ERRORS = {
|
||||||
|
MAPPING: "Mapping Error",
|
||||||
|
UNSUPPORTED_TYPE: "Unsupported type by Notion",
|
||||||
|
};
|
||||||
+2
-2
@@ -10,7 +10,7 @@ const Loading = () => {
|
|||||||
<div className="mt-6 p-6">
|
<div className="mt-6 p-6">
|
||||||
<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 bg-slate-200 select-none">
|
||||||
{t("environments.integrations.notion.link_database")}
|
{t("environments.integrations.notion.link_database")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +48,7 @@ const Loading = () => {
|
|||||||
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
|
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm whitespace-nowrap text-slate-500">
|
||||||
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
|
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center"></div>
|
<div className="text-center"></div>
|
||||||
+2
-2
@@ -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]/project/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
||||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/notion/components/NotionWrapper";
|
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/NotionWrapper";
|
||||||
import {
|
import {
|
||||||
NOTION_AUTH_URL,
|
NOTION_AUTH_URL,
|
||||||
NOTION_OAUTH_CLIENT_ID,
|
NOTION_OAUTH_CLIENT_ID,
|
||||||
+8
-8
@@ -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]/project/integrations/lib/webhook";
|
import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/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";
|
||||||
@@ -79,7 +79,7 @@ const Page = async (props) => {
|
|||||||
disabled: isReadOnly,
|
disabled: isReadOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connectHref: `/environments/${params.environmentId}/project/integrations/webhooks`,
|
connectHref: `/environments/${params.environmentId}/workspace/integrations/webhooks`,
|
||||||
connectText: t("environments.integrations.manage_webhooks"),
|
connectText: t("environments.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",
|
||||||
@@ -93,7 +93,7 @@ const Page = async (props) => {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connectHref: `/environments/${params.environmentId}/project/integrations/google-sheets`,
|
connectHref: `/environments/${params.environmentId}/workspace/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",
|
||||||
@@ -107,7 +107,7 @@ const Page = async (props) => {
|
|||||||
disabled: isReadOnly,
|
disabled: isReadOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connectHref: `/environments/${params.environmentId}/project/integrations/airtable`,
|
connectHref: `/environments/${params.environmentId}/workspace/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",
|
||||||
@@ -121,7 +121,7 @@ const Page = async (props) => {
|
|||||||
disabled: isReadOnly,
|
disabled: isReadOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connectHref: `/environments/${params.environmentId}/project/integrations/slack`,
|
connectHref: `/environments/${params.environmentId}/workspace/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",
|
||||||
@@ -163,7 +163,7 @@ const Page = async (props) => {
|
|||||||
disabled: isReadOnly,
|
disabled: isReadOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
connectHref: `/environments/${params.environmentId}/project/integrations/notion`,
|
connectHref: `/environments/${params.environmentId}/workspace/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",
|
||||||
@@ -196,7 +196,7 @@ const Page = async (props) => {
|
|||||||
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}/project/app-connection`,
|
connectHref: `/environments/${params.environmentId}/workspace/app-connection`,
|
||||||
connectText: t("common.connect"),
|
connectText: t("common.connect"),
|
||||||
connectNewTab: false,
|
connectNewTab: false,
|
||||||
label: "Javascript SDK",
|
label: "Javascript SDK",
|
||||||
@@ -209,7 +209,7 @@ const Page = async (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<PageHeader pageTitle={t("common.project_configuration")}>
|
<PageHeader pageTitle={t("common.workspace_configuration")}>
|
||||||
<ProjectConfigNavigation environmentId={params.environmentId} activeId="integrations" />
|
<ProjectConfigNavigation environmentId={params.environmentId} 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">
|
||||||
+1
-1
@@ -15,7 +15,7 @@ import {
|
|||||||
} from "@formbricks/types/integration/slack";
|
} from "@formbricks/types/integration/slack";
|
||||||
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]/project/integrations/actions";
|
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
||||||
import SlackLogo from "@/images/slacklogo.png";
|
import SlackLogo from "@/images/slacklogo.png";
|
||||||
import { recallToHeadline } from "@/lib/utils/recall";
|
import { recallToHeadline } from "@/lib/utils/recall";
|
||||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import toast from "react-hot-toast";
|
|||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/project/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/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";
|
||||||
+4
-4
@@ -6,10 +6,10 @@ import { TIntegrationItem } from "@formbricks/types/integration";
|
|||||||
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
||||||
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 { getSlackChannelsAction } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/actions";
|
import { getSlackChannelsAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/slack/actions";
|
||||||
import { AddChannelMappingModal } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/AddChannelMappingModal";
|
import { AddChannelMappingModal } from "@/app/(app)/environments/[environmentId]/workspace/integrations/slack/components/AddChannelMappingModal";
|
||||||
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/ManageIntegration";
|
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/workspace/integrations/slack/components/ManageIntegration";
|
||||||
import { authorize } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/lib/slack";
|
import { authorize } from "@/app/(app)/environments/[environmentId]/workspace/integrations/slack/lib/slack";
|
||||||
import slackLogo from "@/images/slacklogo.png";
|
import slackLogo from "@/images/slacklogo.png";
|
||||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||||
|
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/project/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/workspace/integrations/lib/surveys";
|
||||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/project/integrations/slack/components/SlackWrapper";
|
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/workspace/integrations/slack/components/SlackWrapper";
|
||||||
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants";
|
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants";
|
||||||
import { getIntegrationByType } from "@/lib/integration/service";
|
import { getIntegrationByType } from "@/lib/integration/service";
|
||||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
@@ -32,7 +32,7 @@ const Page = async (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/project/integrations`} />
|
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/workspace/integrations`} />
|
||||||
<PageHeader pageTitle={t("environments.integrations.slack.slack_integration")} />
|
<PageHeader pageTitle={t("environments.integrations.slack.slack_integration")} />
|
||||||
<div className="h-[75vh] w-full">
|
<div className="h-[75vh] w-full">
|
||||||
<SlackWrapper
|
<SlackWrapper
|
||||||
@@ -82,6 +82,7 @@ const mockPipelineInput = {
|
|||||||
},
|
},
|
||||||
country: "USA",
|
country: "USA",
|
||||||
action: "Action Name",
|
action: "Action Name",
|
||||||
|
ipAddress: "203.0.113.7",
|
||||||
} as TResponseMeta,
|
} as TResponseMeta,
|
||||||
personAttributes: {},
|
personAttributes: {},
|
||||||
singleUseId: null,
|
singleUseId: null,
|
||||||
@@ -346,7 +347,7 @@ describe("handleIntegrations", () => {
|
|||||||
expect(airtableWriteData).toHaveBeenCalledTimes(1);
|
expect(airtableWriteData).toHaveBeenCalledTimes(1);
|
||||||
// Adjust expectations for metadata and recalled question
|
// Adjust expectations for metadata and recalled question
|
||||||
const expectedMetadataString =
|
const expectedMetadataString =
|
||||||
"Source: web\nURL: http://example.com\nBrowser: Chrome\nOS: Mac OS\nDevice: Desktop\nCountry: USA\nAction: Action Name";
|
"Source: web\nURL: http://example.com\nBrowser: Chrome\nOS: Mac OS\nDevice: Desktop\nCountry: USA\nAction: Action Name\nIP Address: 203.0.113.7";
|
||||||
expect(airtableWriteData).toHaveBeenCalledWith(
|
expect(airtableWriteData).toHaveBeenCalledWith(
|
||||||
mockAirtableIntegration.config.key,
|
mockAirtableIntegration.config.key,
|
||||||
mockAirtableIntegration.config.data[0],
|
mockAirtableIntegration.config.data[0],
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const convertMetaObjectToString = (metadata: TResponseMeta): string => {
|
|||||||
if (metadata.userAgent?.device) result.push(`Device: ${metadata.userAgent.device}`);
|
if (metadata.userAgent?.device) result.push(`Device: ${metadata.userAgent.device}`);
|
||||||
if (metadata.country) result.push(`Country: ${metadata.country}`);
|
if (metadata.country) result.push(`Country: ${metadata.country}`);
|
||||||
if (metadata.action) result.push(`Action: ${metadata.action}`);
|
if (metadata.action) result.push(`Action: ${metadata.action}`);
|
||||||
|
if (metadata.ipAddress) result.push(`IP Address: ${metadata.ipAddress}`);
|
||||||
|
|
||||||
// Join all the elements in the result array with a newline for formatting
|
// Join all the elements in the result array with a newline for formatting
|
||||||
return result.join("\n");
|
return result.join("\n");
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PipelineTriggers, Webhook } from "@prisma/client";
|
import { PipelineTriggers, Webhook } from "@prisma/client";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
import { v7 as uuidv7 } from "uuid";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
@@ -8,6 +9,7 @@ import { ZPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines";
|
|||||||
import { responses } from "@/app/lib/api/response";
|
import { responses } from "@/app/lib/api/response";
|
||||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||||
import { CRON_SECRET } from "@/lib/constants";
|
import { CRON_SECRET } from "@/lib/constants";
|
||||||
|
import { generateStandardWebhookSignature } from "@/lib/crypto";
|
||||||
import { getIntegrations } from "@/lib/integration/service";
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||||
@@ -90,28 +92,50 @@ export const POST = async (request: Request) => {
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const webhookPromises = webhooks.map((webhook) =>
|
const webhookPromises = webhooks.map((webhook) => {
|
||||||
fetchWithTimeout(webhook.url, {
|
const body = JSON.stringify({
|
||||||
method: "POST",
|
webhookId: webhook.id,
|
||||||
headers: { "content-type": "application/json" },
|
event,
|
||||||
body: JSON.stringify({
|
data: {
|
||||||
webhookId: webhook.id,
|
...response,
|
||||||
event,
|
survey: {
|
||||||
data: {
|
title: survey.name,
|
||||||
...response,
|
type: survey.type,
|
||||||
survey: {
|
status: survey.status,
|
||||||
title: survey.name,
|
createdAt: survey.createdAt,
|
||||||
type: survey.type,
|
updatedAt: survey.updatedAt,
|
||||||
status: survey.status,
|
|
||||||
createdAt: survey.createdAt,
|
|
||||||
updatedAt: survey.updatedAt,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate Standard Webhooks headers
|
||||||
|
const webhookMessageId = uuidv7();
|
||||||
|
const webhookTimestamp = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const requestHeaders: Record<string, string> = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"webhook-id": webhookMessageId,
|
||||||
|
"webhook-timestamp": webhookTimestamp.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add signature if webhook has a secret configured
|
||||||
|
if (webhook.secret) {
|
||||||
|
requestHeaders["webhook-signature"] = generateStandardWebhookSignature(
|
||||||
|
webhookMessageId,
|
||||||
|
webhookTimestamp,
|
||||||
|
body,
|
||||||
|
webhook.secret
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchWithTimeout(webhook.url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: requestHeaders,
|
||||||
|
body,
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
logger.error({ error, url: request.url }, `Webhook call to ${webhook.url} failed`);
|
logger.error({ error, url: request.url }, `Webhook call to ${webhook.url} failed`);
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
|
|
||||||
if (event === "responseFinished") {
|
if (event === "responseFinished") {
|
||||||
// Fetch integrations and responseCount in parallel
|
// Fetch integrations and responseCount in parallel
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const GET = async (req: Request) => {
|
|||||||
const result = await createOrUpdateIntegration(environmentId, googleSheetIntegration);
|
const result = await createOrUpdateIntegration(environmentId, googleSheetIntegration);
|
||||||
if (result) {
|
if (result) {
|
||||||
return Response.redirect(
|
return Response.redirect(
|
||||||
`${WEBAPP_URL}/environments/${environmentId}/project/integrations/google-sheets`
|
`${WEBAPP_URL}/environments/${environmentId}/workspace/integrations/google-sheets`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
|
|||||||
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||||
import { getSurvey } from "@/lib/survey/service";
|
import { getSurvey } from "@/lib/survey/service";
|
||||||
|
import { getClientIpFromHeaders } from "@/lib/utils/client-ip";
|
||||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
|
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
|
||||||
import { validateFileUploads } from "@/modules/storage/utils";
|
import { validateFileUploads } from "@/modules/storage/utils";
|
||||||
@@ -136,6 +137,13 @@ export const POST = withV1ApiWrapper({
|
|||||||
action: responseInputData?.meta?.action,
|
action: responseInputData?.meta?.action,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Capture IP address if the survey has IP capture enabled
|
||||||
|
// Server-derived IP always overwrites any client-provided value
|
||||||
|
if (survey.isCaptureIpEnabled) {
|
||||||
|
const ipAddress = await getClientIpFromHeaders();
|
||||||
|
meta.ipAddress = ipAddress;
|
||||||
|
}
|
||||||
|
|
||||||
response = await createResponseWithQuotaEvaluation({
|
response = await createResponseWithQuotaEvaluation({
|
||||||
...responseInputData,
|
...responseInputData,
|
||||||
meta,
|
meta,
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const GET = withV1ApiWrapper({
|
|||||||
await createOrUpdateIntegration(environmentId, airtableIntegrationInput);
|
await createOrUpdateIntegration(environmentId, airtableIntegrationInput);
|
||||||
return {
|
return {
|
||||||
response: Response.redirect(
|
response: Response.redirect(
|
||||||
`${WEBAPP_URL}/environments/${environmentId}/project/integrations/airtable`
|
`${WEBAPP_URL}/environments/${environmentId}/workspace/integrations/airtable`
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ export const GET = withV1ApiWrapper({
|
|||||||
if (result) {
|
if (result) {
|
||||||
return {
|
return {
|
||||||
response: Response.redirect(
|
response: Response.redirect(
|
||||||
`${WEBAPP_URL}/environments/${environmentId}/project/integrations/notion`
|
`${WEBAPP_URL}/environments/${environmentId}/workspace/integrations/notion`
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
return {
|
return {
|
||||||
response: Response.redirect(
|
response: Response.redirect(
|
||||||
`${WEBAPP_URL}/environments/${environmentId}/project/integrations/notion?error=${error}`
|
`${WEBAPP_URL}/environments/${environmentId}/workspace/integrations/notion?error=${error}`
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user