fix: modified google sheet integration and minor refactors in other integrations (#2572)

This commit is contained in:
Dhruwang Jariwala
2024-05-23 15:55:48 +05:30
committed by GitHub
parent 052f86b19f
commit 12a606a443
31 changed files with 525 additions and 453 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -21,7 +21,8 @@ export const metadata = {
The Google Sheets integration allows you to automatically send responses to a Google Sheet of your choice.
<Note>
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow
the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
</Note>
## Connect Google Sheets
@@ -70,7 +71,7 @@ Before the next step, make sure that you have a Formbricks Survey with at least
className="max-w-full rounded-lg sm:max-w-3xl"
/>
6. Select the Google Sheet you want to link with Formbricks and the Survey. On doing so, you will be asked with what questions' responses you want to feed in the Google Sheet. Select the questions and click on the "Link Sheet" button.
6. Enter the spreadsheet URL for the Google Sheet you want to link with Formbricks and the Survey. On doing so, you will be asked with what questions' responses you want to feed in the Google Sheet. Select the questions and click on the "Link Sheet" button.
<MdxImage
src={LinkWithQuestions}
@@ -115,7 +116,6 @@ To remove the integration with Google Account,
For the above, we ask for:
1. **User Email**: To identify you (that's it, nothing else, we're opensource, see this in our codebase [here](https://github.com/formbricks/formbricks/blob/main/apps/web/app/api/google-sheet/callback/route.ts#L47C17-L47C25))
1. **Google Drive API**: To list all your google sheets (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/googleSheet/service.ts#L13))
1. **Google Spreadsheet API**: To write to the spreadsheet you select (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/googleSheet/service.ts#L70))
<Note>We store as little personal information as possible.</Note>

View File

@@ -116,7 +116,7 @@ Integrating Google Sheets with a self-hosted Formbricks instance requires config
1. Go to the **[Google Cloud Console](https://console.cloud.google.com/)** and **create a new project**.
2. Enable necessary APIs:
- Now select the project you just created and go to the **APIs & Services** section.
- Click on the **Enable APIs and Services** button and search for **Google Sheets API** & **Google Drive API** and enable it.
- Click on the **Enable APIs and Services** button and search for **Google Sheets API** and enable it.
3. Configure OAuth Consent Screen:
- Go to **OAuth Consent screen** and select the appropriate User Type (External or Internal). Select **Internal** if you want only the users of your Google Workspace to be able to use the integration.
- Fill the required details:
@@ -128,12 +128,11 @@ Integrating Google Sheets with a self-hosted Formbricks instance requires config
- Click on the **Add or Remove Scopes** button and add the scopes:
- `https://www.googleapis.com/auth/userinfo.email`
- `https://www.googleapis.com/auth/spreadsheets`
- `https://www.googleapis.com/auth/drive`
- Click on the **Update** button. Verify the scopes and click on the **Save and Continue** button.
- Skip the **Test Users** section and click on the **Save and Continue** button.
1. View the OAuth Consent Screen summary and click on the **Back to Dashboard** button.
2. Register OAuth Client:
5. View the OAuth Consent Screen summary and click on the **Back to Dashboard** button.
6. Register OAuth Client:
- Navigate to **Credentials** > **Create Credentials** > **OAuth Client ID**.
- Select **Web Application** and set:
@@ -142,13 +141,10 @@ Integrating Google Sheets with a self-hosted Formbricks instance requires config
- Authorized redirect URIs: `https://<your-public-facing-url>/api/google-sheet/callback`
- Save and note the Client ID and Client Secret.
1. Copy the Client ID and Client Secret and set them as environment variables in your Formbricks instance:
7. Copy the Client ID and Client Secret and set them as environment variables in your Formbricks instance:
- `GOOGLE_SHEETS_CLIENT_ID`
- `GOOGLE_SHEETS_CLIENT_SECRET`
- `GOOGLE_SHEETS_REDIRECT_URL`
2. Enable Google Drive API:
- Go to the **APIs & Services** section and click on the **Enable APIs and Services** button.
- Search for **Google Drive API** and enable it.
Now just copy **GOOGLE_SHEETS_CLIENT_ID**, **GOOGLE_SHEETS_CLIENT_SECRET** and **GOOGLE_SHEETS_REDIRECT_URL** for your integration & add it to your **Formbricks environment variables** as in the docker compose file:

View File

@@ -1,11 +1,13 @@
"use client";
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown";
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
import AirtableLogo from "@/images/airtableLogo.svg";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Control, Controller, UseFormSetValue, useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
@@ -25,8 +27,6 @@ import { Label } from "@formbricks/ui/Label";
import { Modal } from "@formbricks/ui/Modal";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
import AirtableLogo from "../images/airtable.svg";
type EditModeProps =
| { isEditMode: false; defaultData?: never }
| { isEditMode: true; defaultData: IntegrationModalInputs & { index: number } };
@@ -56,69 +56,16 @@ const NoBaseFoundError = () => {
);
};
interface BaseSelectProps {
control: Control<IntegrationModalInputs, any>;
isLoading: boolean;
fetchTable: (val: string) => Promise<void>;
airtableArray: TIntegrationItem[];
setValue: UseFormSetValue<IntegrationModalInputs>;
defaultValue: string | undefined;
}
const BaseSelect = ({
export const AddIntegrationModal = ({
open,
setOpenWithStates,
environmentId,
airtableArray,
control,
fetchTable,
isLoading,
setValue,
defaultValue,
}: BaseSelectProps) => {
return (
<div className="flex w-full flex-col">
<Label htmlFor="base">Airtable base</Label>
<div className="mt-1 flex">
<Controller
control={control}
name="base"
render={({ field }) => (
<Select
required
disabled={isLoading}
onValueChange={async (val) => {
field.onChange(val);
await fetchTable(val);
setValue("table", "");
}}
defaultValue={defaultValue}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{airtableArray.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</div>
</div>
);
};
export const AddIntegrationModal = (props: AddIntegrationModalProps) => {
const {
open,
setOpenWithStates,
environmentId,
airtableArray,
surveys,
airtableIntegration,
isEditMode,
defaultData,
} = props;
surveys,
airtableIntegration,
isEditMode,
defaultData,
}: AddIntegrationModalProps) => {
const router = useRouter();
const [tables, setTables] = useState<TIntegrationAirtableTables["tables"]>([]);
const [isLoading, setIsLoading] = useState(false);
@@ -248,7 +195,7 @@ export const AddIntegrationModal = (props: AddIntegrationModalProps) => {
<div className="flex rounded-lg p-6">
<div className="flex w-full flex-col gap-y-4 pt-5">
{airtableArray.length ? (
<BaseSelect
<BaseSelectDropdown
control={control}
isLoading={isLoading}
fetchTable={fetchTable}

View File

@@ -1,14 +1,15 @@
"use client";
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration";
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
import airtableLogo from "@/images/airtableLogo.svg";
import { useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
import { TSurvey } from "@formbricks/types/surveys";
import { AirtableConnect } from "./Connect";
import { Home } from "./Home";
import { ConnectIntegration } from "@formbricks/ui/ConnectIntegration";
interface AirtableWrapperProps {
environmentId: string;
@@ -16,7 +17,7 @@ interface AirtableWrapperProps {
airtableIntegration?: TIntegrationAirtable;
surveys: TSurvey[];
environment: TEnvironment;
enabled: boolean;
isEnabled: boolean;
webAppUrl: string;
}
@@ -26,19 +27,23 @@ export const AirtableWrapper = ({
airtableIntegration,
surveys,
environment,
enabled,
isEnabled,
webAppUrl,
}: AirtableWrapperProps) => {
const [isConnected, setIsConnected_] = useState(
const [isConnected, setIsConnected] = useState(
airtableIntegration ? airtableIntegration.config?.key : false
);
const setIsConnected = (data: boolean) => {
setIsConnected_(data);
const handleAirtableAuthorization = async () => {
authorize(environmentId, webAppUrl).then((url: string) => {
if (url) {
window.location.replace(url);
}
});
};
return isConnected && airtableIntegration ? (
<Home
<ManageIntegration
airtableArray={airtableArray}
environmentId={environmentId}
environment={environment}
@@ -47,6 +52,11 @@ export const AirtableWrapper = ({
surveys={surveys}
/>
) : (
<AirtableConnect enabled={enabled} environmentId={environment.id} webAppUrl={webAppUrl} />
<ConnectIntegration
isEnabled={isEnabled}
integrationType={"airtable"}
handleAuthorization={handleAirtableAuthorization}
integrationLogoSrc={airtableLogo}
/>
);
};

View File

@@ -0,0 +1,59 @@
import { Control, Controller, UseFormSetValue } from "react-hook-form";
import { TIntegrationItem } from "@formbricks/types/integration";
import { Label } from "@formbricks/ui/Label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
import { IntegrationModalInputs } from "./AddIntegrationModal";
interface BaseSelectProps {
control: Control<IntegrationModalInputs, any>;
isLoading: boolean;
fetchTable: (val: string) => Promise<void>;
airtableArray: TIntegrationItem[];
setValue: UseFormSetValue<IntegrationModalInputs>;
defaultValue: string | undefined;
}
export const BaseSelectDropdown = ({
airtableArray,
control,
fetchTable,
isLoading,
setValue,
defaultValue,
}: BaseSelectProps) => {
return (
<div className="flex w-full flex-col">
<Label htmlFor="base">Airtable base</Label>
<div className="mt-1 flex">
<Controller
control={control}
name="base"
render={({ field }) => (
<Select
required
disabled={isLoading}
onValueChange={async (val) => {
field.onChange(val);
await fetchTable(val);
setValue("table", "");
}}
defaultValue={defaultValue}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{airtableArray.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</div>
</div>
);
};

View File

@@ -1,59 +0,0 @@
"use client";
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
import FormbricksLogo from "@/images/logo.svg";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { Button } from "@formbricks/ui/Button";
import AirtableLogo from "../images/airtable.svg";
interface AirtableConnectProps {
enabled: boolean;
environmentId: string;
webAppUrl: string;
}
export const AirtableConnect = ({ environmentId, enabled, webAppUrl }: AirtableConnectProps) => {
const [isConnecting, setIsConnecting] = useState(false);
const handleGoogleLogin = async () => {
setIsConnecting(true);
authorize(environmentId, webAppUrl).then((url: string) => {
if (url) {
window.location.replace(url);
}
});
};
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex w-1/2 flex-col items-center justify-center rounded-lg bg-white p-8 shadow">
<div className="flex w-1/2 justify-center -space-x-4">
<div className="flex h-32 w-32 items-center justify-center rounded-full bg-white p-4 shadow-md">
<Image className="w-1/2" src={FormbricksLogo} alt="Formbricks Logo" />
</div>
<div className="flex h-32 w-32 items-center justify-center rounded-full bg-white p-4 shadow-md">
<Image className="w-1/2" src={AirtableLogo} alt="Airtable Logo" />
</div>
</div>
<p className="my-8">Sync responses directly with Airtable.</p>
{!enabled && (
<p className="mb-8 rounded border-slate-200 bg-slate-100 p-3 text-sm">
Airtable Integration is not configured in your instance of Formbricks.
<br />
Please follow the{" "}
<Link href="https://formbricks.com/docs/self-hosting/integrations#airtable" className="underline">
docs
</Link>{" "}
to configure it.
</p>
)}
<Button variant="darkCTA" loading={isConnecting} onClick={handleGoogleLogin} disabled={!enabled}>
Connect with Airtable
</Button>
</div>
</div>
);
};

View File

@@ -17,7 +17,7 @@ import { Button } from "@formbricks/ui/Button";
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
import { EmptySpaceFiller } from "@formbricks/ui/EmptySpaceFiller";
interface handleModalProps {
interface ManageIntegrationProps {
airtableIntegration: TIntegrationAirtable;
environment: TEnvironment;
environmentId: string;
@@ -28,7 +28,7 @@ interface handleModalProps {
const tableHeaders = ["Survey", "Table Name", "Questions", "Updated At"];
export const Home = (props: handleModalProps) => {
export const ManageIntegration = (props: ManageIntegrationProps) => {
const { airtableIntegration, environment, environmentId, setIsConnected, surveys, airtableArray } = props;
const [isDeleting, setisDeleting] = useState(false);
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);

View File

@@ -13,7 +13,7 @@ import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
const Page = async ({ params }) => {
const enabled = !!AIRTABLE_CLIENT_ID;
const isEnabled = !!AIRTABLE_CLIENT_ID;
const [surveys, integrations, environment] = await Promise.all([
getSurveys(params.environmentId),
getIntegrations(params.environmentId),
@@ -42,7 +42,7 @@ const Page = async ({ params }) => {
<PageHeader pageTitle="Airtable Integration" />
<div className="h-[75vh] w-full">
<AirtableWrapper
enabled={enabled}
isEnabled={isEnabled}
airtableIntegration={airtableIntegration}
airtableArray={airtableArray}
environmentId={environment.id}

View File

@@ -4,15 +4,20 @@ import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getSpreadSheets } from "@formbricks/lib/googleSheet/service";
import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { TIntegrationGoogleSheetsCredential } from "@formbricks/types/integration/googleSheet";
export const refreshSheetAction = async (environmentId: string) => {
export async function getSpreadsheetNameByIdAction(
credentials: TIntegrationGoogleSheetsCredential,
environmentId: string,
spreadsheetId: string
) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
return await getSpreadSheets(environmentId);
};
return await getSpreadsheetNameById(credentials, spreadsheetId);
}

View File

@@ -1,4 +1,11 @@
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
import { getSpreadsheetNameByIdAction } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions";
import {
constructGoogleSheetsUrl,
extractSpreadsheetIdFromUrl,
isValidGoogleSheetsUrl,
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
import Image from "next/image";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -6,7 +13,6 @@ import toast from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
import { TIntegrationItem } from "@formbricks/types/integration";
import {
TIntegrationGoogleSheets,
TIntegrationGoogleSheetsConfigData,
@@ -16,17 +22,15 @@ import { TSurvey } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Checkbox } from "@formbricks/ui/Checkbox";
import { DropdownSelector } from "@formbricks/ui/DropdownSelector";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { Modal } from "@formbricks/ui/Modal";
import GoogleSheetLogo from "../images/google-sheets-small.png";
interface AddWebhookModalProps {
interface AddIntegrationModalProps {
environmentId: string;
open: boolean;
surveys: TSurvey[];
setOpen: (v: boolean) => void;
spreadsheets: TIntegrationItem[];
googleSheetIntegration: TIntegrationGoogleSheets;
selectedIntegration?: (TIntegrationGoogleSheetsConfigData & { index: number }) | null;
}
@@ -36,12 +40,9 @@ export const AddIntegrationModal = ({
surveys,
open,
setOpen,
spreadsheets,
googleSheetIntegration,
selectedIntegration,
}: AddWebhookModalProps) => {
const { handleSubmit } = useForm();
}: AddIntegrationModalProps) => {
const integrationData = {
spreadsheetId: "",
spreadsheetName: "",
@@ -51,11 +52,11 @@ export const AddIntegrationModal = ({
questions: "",
createdAt: new Date(),
};
const { handleSubmit } = useForm();
const [selectedQuestions, setSelectedQuestions] = useState<string[]>([]);
const [isLinkingSheet, setIsLinkingSheet] = useState(false);
const [selectedSurvey, setSelectedSurvey] = useState<TSurvey | null>(null);
const [selectedSpreadsheet, setSelectedSpreadsheet] = useState<any>(null);
const [spreadsheetUrl, setSpreadsheetUrl] = useState("");
const [isDeleting, setIsDeleting] = useState<boolean>(false);
const existingIntegrationData = googleSheetIntegration?.config?.data;
const googleSheetIntegrationData: TIntegrationGoogleSheetsInput = {
@@ -78,10 +79,7 @@ export const AddIntegrationModal = ({
useEffect(() => {
if (selectedIntegration) {
setSelectedSpreadsheet({
id: selectedIntegration.spreadsheetId,
name: selectedIntegration.spreadsheetName,
});
setSpreadsheetUrl(constructGoogleSheetsUrl(selectedIntegration.spreadsheetId));
setSelectedSurvey(
surveys.find((survey) => {
return survey.id === selectedIntegration.surveyId;
@@ -89,25 +87,32 @@ export const AddIntegrationModal = ({
);
setSelectedQuestions(selectedIntegration.questionIds);
return;
} else {
setSpreadsheetUrl("");
}
resetForm();
}, [selectedIntegration, surveys]);
const linkSheet = async () => {
try {
if (!selectedSpreadsheet) {
throw new Error("Please select a spreadsheet");
if (isValidGoogleSheetsUrl(spreadsheetUrl)) {
throw new Error("Please enter a valid spreadsheet url");
}
if (!selectedSurvey) {
throw new Error("Please select a survey");
}
if (selectedQuestions.length === 0) {
throw new Error("Please select at least one question");
}
const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl);
const spreadsheetName = await getSpreadsheetNameByIdAction(
googleSheetIntegration.config.key,
environmentId,
spreadsheetId
);
setIsLinkingSheet(true);
integrationData.spreadsheetId = selectedSpreadsheet.id;
integrationData.spreadsheetName = selectedSpreadsheet.name;
integrationData.spreadsheetId = spreadsheetId;
integrationData.spreadsheetName = spreadsheetName;
integrationData.surveyId = selectedSurvey.id;
integrationData.surveyName = selectedSurvey.name;
integrationData.questionIds = selectedQuestions;
@@ -148,7 +153,6 @@ export const AddIntegrationModal = ({
const resetForm = () => {
setIsLinkingSheet(false);
setSelectedSpreadsheet("");
setSelectedSurvey(null);
};
@@ -166,15 +170,8 @@ export const AddIntegrationModal = ({
}
};
const hasMatchingId = googleSheetIntegration.config.data.some((configData) => {
if (!selectedSpreadsheet) {
return false;
}
return configData.spreadsheetId === selectedSpreadsheet.id;
});
return (
<Modal open={open} setOpen={setOpenWithStates} noPadding closeOnOutsideClick={false}>
<Modal open={open} setOpen={setOpenWithStates} noPadding closeOnOutsideClick={true}>
<div className="flex h-full flex-col rounded-lg">
<div className="rounded-t-lg bg-slate-100">
<div className="flex w-full items-center justify-between p-6">
@@ -194,23 +191,13 @@ export const AddIntegrationModal = ({
<div className="w-full space-y-4">
<div>
<div className="mb-4">
<DropdownSelector
label="Select Spreadsheet"
items={spreadsheets}
selectedItem={selectedSpreadsheet}
setSelectedItem={setSelectedSpreadsheet}
disabled={spreadsheets.length === 0}
<Label>Spreadsheet URL</Label>
<Input
value={spreadsheetUrl}
onChange={(e) => setSpreadsheetUrl(e.target.value)}
placeholder="https://docs.google.com/spreadsheets/d/<your-spreadsheet-id>"
className="mt-1"
/>
{selectedSpreadsheet && hasMatchingId && (
<p className="text-xs text-amber-700">
<strong>Warning:</strong> You have already connected one survey with this sheet. Your
data will be inconsistent
</p>
)}
<p className="m-1 text-xs text-slate-500">
{spreadsheets.length === 0 &&
"You have to create at least one spreadshseet to be able to setup this integration"}
</p>
</div>
<div>
<DropdownSelector

View File

@@ -1,61 +0,0 @@
"use client";
import FormbricksLogo from "@/images/logo.svg";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { Button } from "@formbricks/ui/Button";
import GoogleSheetLogo from "../images/google-sheets-small.png";
import { authorize } from "../lib/google";
interface ConnectProps {
enabled: boolean;
environmentId: string;
webAppUrl: string;
}
export const Connect = ({ enabled, environmentId, webAppUrl }: ConnectProps) => {
const [isConnecting, setIsConnecting] = useState(false);
const handleGoogleLogin = async () => {
setIsConnecting(true);
authorize(environmentId, webAppUrl).then((url: string) => {
if (url) {
window.location.replace(url);
}
});
};
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex w-1/2 flex-col items-center justify-center rounded-lg bg-white p-8 shadow">
<div className="flex w-1/2 justify-center -space-x-4">
<div className="flex h-32 w-32 items-center justify-center rounded-full bg-white p-4 shadow-md">
<Image className="w-1/2" src={FormbricksLogo} alt="Formbricks Logo" />
</div>
<div className="flex h-32 w-32 items-center justify-center rounded-full bg-white p-4 shadow-md">
<Image className="w-1/2" src={GoogleSheetLogo} alt="Google Sheet logo" />
</div>
</div>
<p className="my-8">Sync responses directly with Google Sheets.</p>
{!enabled && (
<p className="mb-8 rounded border-slate-200 bg-slate-100 p-3 text-sm">
Google Sheets Integration is not configured in your instance of Formbricks.
<br />
Please follow the{" "}
<Link
href="https://formbricks.com/docs/self-hosting/integrations#google-sheets"
className="underline">
docs
</Link>{" "}
to configure it.
</p>
)}
<Button variant="darkCTA" loading={isConnecting} onClick={handleGoogleLogin} disabled={!enabled}>
Connect with Google
</Button>
</div>
</div>
);
};

View File

@@ -1,49 +1,49 @@
"use client";
import { refreshSheetAction } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions";
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration";
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/google";
import googleSheetLogo from "@/images/googleSheetsLogo.png";
import { useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationItem } from "@formbricks/types/integration";
import {
TIntegrationGoogleSheets,
TIntegrationGoogleSheetsConfigData,
} from "@formbricks/types/integration/googleSheet";
import { TSurvey } from "@formbricks/types/surveys";
import { ConnectIntegration } from "@formbricks/ui/ConnectIntegration";
import { AddIntegrationModal } from "./AddIntegrationModal";
import { Connect } from "./Connect";
import { Home } from "./Home";
interface GoogleSheetWrapperProps {
enabled: boolean;
isEnabled: boolean;
environment: TEnvironment;
surveys: TSurvey[];
spreadSheetArray: TIntegrationItem[];
googleSheetIntegration?: TIntegrationGoogleSheets;
webAppUrl: string;
}
export const GoogleSheetWrapper = ({
enabled,
isEnabled,
environment,
surveys,
spreadSheetArray,
googleSheetIntegration,
webAppUrl,
}: GoogleSheetWrapperProps) => {
const [isConnected, setIsConnected] = useState(
googleSheetIntegration ? googleSheetIntegration.config?.key : false
);
const [spreadsheets, setSpreadsheets] = useState(spreadSheetArray);
const [isModalOpen, setModalOpen] = useState<boolean>(false);
const [selectedIntegration, setSelectedIntegration] = useState<
(TIntegrationGoogleSheetsConfigData & { index: number }) | null
>(null);
const refreshSheet = async () => {
const latestSpreadsheets = await refreshSheetAction(environment.id);
setSpreadsheets(latestSpreadsheets);
const handleGoogleAuthorization = async () => {
authorize(environment.id, webAppUrl).then((url: string) => {
if (url) {
window.location.replace(url);
}
});
};
return (
@@ -55,21 +55,24 @@ export const GoogleSheetWrapper = ({
surveys={surveys}
open={isModalOpen}
setOpen={setModalOpen}
spreadsheets={spreadsheets}
googleSheetIntegration={googleSheetIntegration}
selectedIntegration={selectedIntegration}
/>
<Home
<ManageIntegration
environment={environment}
googleSheetIntegration={googleSheetIntegration}
setOpenAddIntegrationModal={setModalOpen}
setIsConnected={setIsConnected}
setSelectedIntegration={setSelectedIntegration}
refreshSheet={refreshSheet}
/>
</>
) : (
<Connect enabled={enabled} environmentId={environment.id} webAppUrl={webAppUrl} />
<ConnectIntegration
isEnabled={isEnabled}
integrationType={"googleSheets"}
handleAuthorization={handleGoogleAuthorization}
integrationLogoSrc={googleSheetLogo}
/>
)}
</>
);

View File

@@ -15,23 +15,21 @@ import { Button } from "@formbricks/ui/Button";
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
import { EmptySpaceFiller } from "@formbricks/ui/EmptySpaceFiller";
interface HomeProps {
interface ManageIntegrationProps {
environment: TEnvironment;
googleSheetIntegration: TIntegrationGoogleSheets;
setOpenAddIntegrationModal: (v: boolean) => void;
setIsConnected: (v: boolean) => void;
setSelectedIntegration: (v: (TIntegrationGoogleSheetsConfigData & { index: number }) | null) => void;
refreshSheet: () => void;
}
export const Home = ({
export const ManageIntegration = ({
environment,
googleSheetIntegration,
setOpenAddIntegrationModal,
setIsConnected,
setSelectedIntegration,
refreshSheet,
}: HomeProps) => {
}: ManageIntegrationProps) => {
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
const integrationArray = googleSheetIntegration
? googleSheetIntegration.config.data
@@ -72,7 +70,6 @@ export const Home = ({
<Button
variant="darkCTA"
onClick={() => {
refreshSheet();
setSelectedIntegration(null);
setOpenAddIntegrationModal(true);
}}>
@@ -102,7 +99,7 @@ export const Home = ({
return (
<div
key={index}
className="m-2 grid h-16 grid-cols-8 content-center rounded-lg hover:bg-slate-100"
className="m-2 grid h-16 cursor-pointer grid-cols-8 content-center rounded-lg hover:bg-slate-100"
onClick={() => {
editIntegration(index);
}}>

View File

@@ -0,0 +1,20 @@
export const extractSpreadsheetIdFromUrl = (url: string): string => {
const regex = /\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/;
const match = url.match(regex);
if (match && match[1]) {
return match[1];
} else {
throw new Error("Invalid Google Sheets URL");
}
};
export const constructGoogleSheetsUrl = (spreadsheetId: string): string => {
const baseUrl = "https://docs.google.com/spreadsheets/d/";
return baseUrl + spreadsheetId;
};
export const isValidGoogleSheetsUrl = (url: string): boolean => {
// Regular expression to match Google Sheets URL format
const googleSheetsUrlRegex = /^https:\/\/docs\.google\.com\/spreadsheets\/d\/[a-zA-Z0-9-_]+\/?$/;
return googleSheetsUrlRegex.test(url);
};

View File

@@ -7,18 +7,16 @@ import {
WEBAPP_URL,
} from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getSpreadSheets } from "@formbricks/lib/googleSheet/service";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/googleSheet";
import { GoBackButton } from "@formbricks/ui/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
const Page = async ({ params }) => {
const enabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
const [surveys, integrations, environment] = await Promise.all([
getSurveys(params.environmentId),
getIntegrations(params.environmentId),
@@ -35,20 +33,16 @@ const Page = async ({ params }) => {
const googleSheetIntegration: TIntegrationGoogleSheets | undefined = integrations?.find(
(integration): integration is TIntegrationGoogleSheets => integration.type === "googleSheets"
);
let spreadSheetArray: TIntegrationItem[] = [];
if (googleSheetIntegration && googleSheetIntegration.config.key) {
spreadSheetArray = await getSpreadSheets(params.environmentId);
}
return (
<PageContentWrapper>
<GoBackButton url={`${WEBAPP_URL}/environments/${params.environmentId}/integrations`} />
<PageHeader pageTitle="Google Sheets Integration" />
<div className="h-[75vh] w-full">
<GoogleSheetWrapper
enabled={enabled}
isEnabled={isEnabled}
environment={environment}
surveys={surveys}
spreadSheetArray={spreadSheetArray}
googleSheetIntegration={googleSheetIntegration}
webAppUrl={WEBAPP_URL}
/>

View File

@@ -10,7 +10,7 @@ import { Button } from "@formbricks/ui/Button";
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
import { EmptySpaceFiller } from "@formbricks/ui/EmptySpaceFiller";
interface HomeProps {
interface ManageIntegrationProps {
environment: TEnvironment;
notionIntegration: TIntegrationNotion;
setOpenAddIntegrationModal: React.Dispatch<React.SetStateAction<boolean>>;
@@ -20,13 +20,13 @@ interface HomeProps {
>;
}
export const Home = ({
export const ManageIntegration = ({
environment,
notionIntegration,
setOpenAddIntegrationModal,
setIsConnected,
setSelectedIntegration,
}: HomeProps) => {
}: ManageIntegrationProps) => {
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
const [isDeleting, setisDeleting] = useState(false);
const integrationArray = notionIntegration

View File

@@ -1,8 +1,8 @@
"use client";
import { AddIntegrationModal } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/AddIntegrationModal";
import { Connect } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/Connect";
import { Home } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/Home";
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/ManageIntegration";
import notionLogo from "@/images/notion.png";
import { useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
@@ -12,6 +12,9 @@ import {
TIntegrationNotionDatabase,
} from "@formbricks/types/integration/notion";
import { TSurvey } from "@formbricks/types/surveys";
import { ConnectIntegration } from "@formbricks/ui/ConnectIntegration";
import { authorize } from "../lib/notion";
interface NotionWrapperProps {
notionIntegration: TIntegrationNotion | undefined;
@@ -38,6 +41,14 @@ export const NotionWrapper = ({
(TIntegrationNotionConfigData & { index: number }) | null
>(null);
const handleNotionAuthorization = async () => {
authorize(environment.id, webAppUrl).then((url: string) => {
if (url) {
window.location.replace(url);
}
});
};
return (
<>
{isConnected && notionIntegration ? (
@@ -51,7 +62,7 @@ export const NotionWrapper = ({
databases={databasesArray}
selectedIntegration={selectedIntegration}
/>
<Home
<ManageIntegration
environment={environment}
notionIntegration={notionIntegration}
setOpenAddIntegrationModal={setModalOpen}
@@ -60,7 +71,12 @@ export const NotionWrapper = ({
/>
</>
) : (
<Connect enabled={enabled} environmentId={environment.id} webAppUrl={webAppUrl} />
<ConnectIntegration
isEnabled={enabled}
integrationType={"notion"}
handleAuthorization={handleNotionAuthorization}
integrationLogoSrc={notionLogo}
/>
)}
</>
);

View File

@@ -1,3 +1,5 @@
import AirtableLogo from "@/images/airtableLogo.svg";
import GoogleSheetsLogo from "@/images/googleSheetsLogo.png";
import JsLogo from "@/images/jslogo.png";
import MakeLogo from "@/images/make-small.png";
import n8nLogo from "@/images/n8n.png";
@@ -21,9 +23,6 @@ import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
import AirtableLogo from "./airtable/images/airtable.svg";
import GoogleSheetsLogo from "./google-sheets/images/google-sheets-small.png";
const Page = async ({ params }) => {
const environmentId = params.environmentId;

View File

@@ -1,3 +1,4 @@
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
import SlackLogo from "@/images/slacklogo.png";
import Image from "next/image";
import { useEffect, useMemo, useState } from "react";
@@ -19,8 +20,6 @@ import { DropdownSelector } from "@formbricks/ui/DropdownSelector";
import { Label } from "@formbricks/ui/Label";
import { Modal } from "@formbricks/ui/Modal";
import { createOrUpdateIntegrationAction } from "../../actions";
interface AddChannelMappingModalProps {
environmentId: string;
surveys: TSurvey[];

View File

@@ -1,71 +0,0 @@
"use client";
import FormbricksLogo from "@/images/logo.svg";
import SlackLogo from "@/images/slacklogo.png";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { Button } from "@formbricks/ui/Button";
import { authorize } from "../lib/slack";
interface ConnectProps {
isEnabled: boolean;
environmentId: string;
webAppUrl: string;
}
export const Connect = ({ isEnabled, environmentId, webAppUrl }: ConnectProps) => {
const searchParams = useSearchParams();
const [isConnecting, setIsConnecting] = useState(false);
useEffect(() => {
const error = searchParams?.get("error");
if (error) {
toast.error("Connecting integration failed. Please try again!");
}
}, [searchParams]);
const handleAuthorizeSlack = async () => {
setIsConnecting(true);
authorize(environmentId, webAppUrl).then((url: string) => {
if (url) {
window.location.replace(url);
}
});
};
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex w-1/2 flex-col items-center justify-center rounded-lg bg-white p-8 shadow">
<div className="flex w-1/2 justify-center -space-x-4">
<div className="flex h-32 w-32 items-center justify-center rounded-full bg-white p-4 shadow-md">
<Image className="w-1/2" src={FormbricksLogo} alt="Formbricks Logo" />
</div>
<div className="flex h-32 w-32 items-center justify-center rounded-full bg-white p-4 shadow-md">
<Image className="w-1/2" src={SlackLogo} alt="Slack logo" />
</div>
</div>
<p className="my-8">Send responses directly to Slack.</p>
{!isEnabled && (
<p className="mb-8 rounded border-gray-200 bg-gray-100 p-3 text-sm">
Slack Integration is not configured in your instance of Formbricks.
<br />
Please follow the{" "}
<Link href="https://formbricks.com/docs/self-hosting/integrations#slack" className="underline">
docs
</Link>{" "}
to configure it.
</p>
)}
<Button variant="darkCTA" loading={isConnecting} onClick={handleAuthorizeSlack} disabled={!isEnabled}>
Connect with Slack
</Button>
</div>
</div>
);
};

View File

@@ -1,5 +1,6 @@
"use client";
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
import { Trash2Icon } from "lucide-react";
import React, { useState } from "react";
import toast from "react-hot-toast";
@@ -11,9 +12,7 @@ import { Button } from "@formbricks/ui/Button";
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
import { EmptySpaceFiller } from "@formbricks/ui/EmptySpaceFiller";
import { deleteIntegrationAction } from "../../actions";
interface HomeProps {
interface ManageIntegrationProps {
environment: TEnvironment;
slackIntegration: TIntegrationSlack;
setOpenAddIntegrationModal: React.Dispatch<React.SetStateAction<boolean>>;
@@ -24,14 +23,14 @@ interface HomeProps {
refreshChannels: () => void;
}
export const Home = ({
export const ManageIntegration = ({
environment,
slackIntegration,
setOpenAddIntegrationModal,
setIsConnected,
setSelectedIntegration,
refreshChannels,
}: HomeProps) => {
}: ManageIntegrationProps) => {
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
const [isDeleting, setisDeleting] = useState(false);
const integrationArray = slackIntegration

View File

@@ -1,16 +1,17 @@
"use client";
import { refreshChannelsAction } from "@/app/(app)/environments/[environmentId]/integrations/slack/actions";
import { AddChannelMappingModal } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/AddChannelMappingModal";
import { Connect } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/Connect";
import { Home } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/Home";
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration";
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/slack/lib/slack";
import slackLogo from "@/images/slacklogo.png";
import { useState } from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
import { TSurvey } from "@formbricks/types/surveys";
import { refreshChannelsAction } from "../actions";
import { ConnectIntegration } from "@formbricks/ui/ConnectIntegration";
interface SlackWrapperProps {
isEnabled: boolean;
@@ -41,6 +42,14 @@ export const SlackWrapper = ({
setSlackChannels(latestSlackChannels);
};
const handleSlackAuthorization = async () => {
authorize(environment.id, webAppUrl).then((url: string) => {
if (url) {
window.location.replace(url);
}
});
};
return isConnected && slackIntegration ? (
<>
<AddChannelMappingModal
@@ -52,7 +61,7 @@ export const SlackWrapper = ({
slackIntegration={slackIntegration}
selectedIntegration={selectedIntegration}
/>
<Home
<ManageIntegration
environment={environment}
slackIntegration={slackIntegration}
setOpenAddIntegrationModal={setModalOpen}
@@ -62,6 +71,11 @@ export const SlackWrapper = ({
/>
</>
) : (
<Connect isEnabled={isEnabled} environmentId={environment.id} webAppUrl={webAppUrl} />
<ConnectIntegration
isEnabled={isEnabled}
integrationType={"slack"}
handleAuthorization={handleSlackAuthorization}
integrationLogoSrc={slackLogo}
/>
);
};

View File

@@ -13,7 +13,6 @@ import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
const scopes = [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/userinfo.email",
];

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -4,11 +4,8 @@ import { Prisma } from "@prisma/client";
import { z } from "zod";
import { ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, UnknownError } from "@formbricks/types/errors";
import { TIntegrationItem } from "@formbricks/types/integration";
import {
TIntegrationGoogleSheets,
TIntegrationGoogleSheetsCredential,
ZIntegrationGoogleSheetsCredential,
} from "@formbricks/types/integration/googleSheet";
@@ -18,45 +15,10 @@ import {
GOOGLE_SHEETS_CLIENT_SECRET,
GOOGLE_SHEETS_REDIRECT_URL,
} from "../constants";
import { getIntegrationByType } from "../integration/service";
import { validateInputs } from "../utils/validate";
const { google } = require("googleapis");
const fetchSpreadsheets = async (auth: any) => {
const authClient = authorize(auth);
const service = google.drive({ version: "v3", auth: authClient });
try {
const res = await service.files.list({
q: "mimeType='application/vnd.google-apps.spreadsheet' AND trashed=false",
fields: "nextPageToken, files(id, name)",
});
return res.data.files;
} catch (err) {
throw err;
}
};
export const getSpreadSheets = async (environmentId: string): Promise<TIntegrationItem[]> => {
validateInputs([environmentId, ZId]);
let spreadsheets: TIntegrationItem[] = [];
try {
const googleIntegration = (await getIntegrationByType(
environmentId,
"googleSheets"
)) as TIntegrationGoogleSheets;
if (googleIntegration && googleIntegration.config?.key) {
spreadsheets = await fetchSpreadsheets(googleIntegration.config?.key);
}
return spreadsheets;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const writeData = async (
credentials: TIntegrationGoogleSheetsCredential,
spreadsheetId: string,
@@ -108,6 +70,34 @@ export const writeData = async (
}
};
export const getSpreadsheetNameById = async (
credentials: TIntegrationGoogleSheetsCredential,
spreadsheetId: string
): Promise<string> => {
validateInputs([credentials, ZIntegrationGoogleSheetsCredential]);
try {
const authClient = authorize(credentials);
const sheets = google.sheets({ version: "v4", auth: authClient });
return new Promise((resolve, reject) => {
sheets.spreadsheets.get({ spreadsheetId }, (err, response) => {
if (err) {
reject(new UnknownError(`Error while fetching spreadsheet data: ${err.message}`));
return;
}
const spreadsheetTitle = response.data.properties.title;
resolve(spreadsheetTitle);
});
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
const authorize = (credentials: any) => {
const client_id = GOOGLE_SHEETS_CLIENT_ID;
const client_secret = GOOGLE_SHEETS_CLIENT_SECRET;

View File

@@ -1,24 +1,41 @@
import FormbricksLogo from "@/images/logo.svg";
import NotionLogo from "@/images/notion.png";
import Image from "next/image";
import Image, { StaticImageData } from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { Button } from "@formbricks/ui/Button";
import { TIntegrationType } from "@formbricks/types/integration";
import { authorize } from "../lib/notion";
import { Button } from "../../ui/Button";
import { FormbricksLogo } from "../FormbricksLogo";
import { getIntegrationDetails } from "./lib/utils";
interface ConnectProps {
enabled: boolean;
environmentId: string;
webAppUrl: string;
interface ConnectIntegrationProps {
isEnabled: boolean;
integrationType: TIntegrationType;
handleAuthorization: () => void;
integrationLogoSrc: string | StaticImageData;
}
export const Connect = ({ enabled, environmentId, webAppUrl }: ConnectProps) => {
export const ConnectIntegration = ({
isEnabled,
integrationType,
handleAuthorization,
integrationLogoSrc,
}: ConnectIntegrationProps) => {
const [isConnecting, setIsConnecting] = useState(false);
const searchParams = useSearchParams();
const integrationDetails = getIntegrationDetails(integrationType);
const handleConnect = () => {
try {
setIsConnecting(true);
handleAuthorization();
} catch (error) {
console.error(error);
setIsConnecting(false);
}
};
useEffect(() => {
const error = searchParams?.get("error");
@@ -28,40 +45,31 @@ export const Connect = ({ enabled, environmentId, webAppUrl }: ConnectProps) =>
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleAuthorizeNotion = async () => {
setIsConnecting(true);
authorize(environmentId, webAppUrl).then((url: string) => {
if (url) {
window.location.replace(url);
}
});
};
return (
<div className="flex h-[75vh] w-full items-center justify-center">
<div className="flex w-1/2 flex-col items-center justify-center rounded-lg bg-white p-8 shadow">
<div className="flex w-1/2 justify-center -space-x-4">
<div className="flex h-32 w-32 items-center justify-center rounded-full bg-white p-4 shadow-md">
<Image className="w-1/2" src={FormbricksLogo} alt="Formbricks Logo" />
<div className="flex h-32 w-32 items-center justify-center rounded-full bg-white p-6 shadow-md">
<FormbricksLogo />
</div>
<div className="flex h-32 w-32 items-center justify-center rounded-full bg-white p-4 shadow-md">
<Image className="w-1/2" src={NotionLogo} alt="Google Sheet logo" />
<Image className="w-1/2" src={integrationLogoSrc} alt="logo" />
</div>
</div>
<p className="my-8">Sync responses directly with your Notion database.</p>
{!enabled && (
<p className="my-8">{integrationDetails?.text}</p>
{!isEnabled && (
<p className="mb-8 rounded border-slate-200 bg-slate-100 p-3 text-sm">
Notion Integration is not configured in your instance of Formbricks.
{integrationDetails?.notConfiguredText}
<br />
Please follow the{" "}
<Link href="https://formbricks.com/docs/self-hosting/integrations#notion" className="underline">
<Link href={integrationDetails?.docsLink ?? ""} className="underline">
docs
</Link>{" "}
to configure it.
</p>
)}
<Button variant="darkCTA" loading={isConnecting} onClick={handleAuthorizeNotion} disabled={!enabled}>
Connect with Notion
<Button variant="darkCTA" loading={isConnecting} onClick={handleConnect} disabled={!isEnabled}>
{integrationDetails?.connectButtonLabel}
</Button>
</div>
</div>

View File

@@ -0,0 +1,34 @@
import { TIntegrationType } from "@formbricks/types/integration";
export const getIntegrationDetails = (integrationType: TIntegrationType) => {
switch (integrationType) {
case "googleSheets":
return {
text: "Sync responses directly with Google Sheets.",
docsLink: "https://formbricks.com/docs/integrations/google-sheets",
connectButtonLabel: "Connect with Google Sheets",
notConfiguredText: "Google Sheet Integration is not configured in your instance of Formbricks.",
};
case "airtable":
return {
text: "Sync responses directly with Airtable.",
docsLink: "https://formbricks.com/docs/integrations/airtable",
connectButtonLabel: "Connect with Airtable",
notConfiguredText: "Airtable Integration is not configured in your instance of Formbricks.",
};
case "notion":
return {
text: "Sync responses directly with your Notion database.",
docsLink: "https://formbricks.com/docs/integrations/notion",
connectButtonLabel: "Connect with Notion",
notConfiguredText: "Notion Integration is not configured in your instance of Formbricks.",
};
case "slack":
return {
text: "Send responses directly to Slack.",
docsLink: "https://formbricks.com/docs/integrations/slack",
connectButtonLabel: "Connect with Slack",
notConfiguredText: "Slack Integration is not configured in your instance of Formbricks.",
};
}
};

View File

@@ -0,0 +1,187 @@
export const FormbricksLogo = () => {
return (
<svg width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M39.1602 147.334H95.8321V175.67C95.8321 191.32 83.1457 204.006 67.4962 204.006C51.8466 204.006 39.1602 191.32 39.1602 175.67V147.334Z"
fill="url(#paint0_linear_415_2)"
/>
<path
d="M39.1602 81.8071H152.504C168.154 81.8071 180.84 94.4936 180.84 110.143C180.84 125.793 168.154 138.479 152.504 138.479H39.1602V81.8071Z"
fill="url(#paint1_linear_415_2)"
/>
<path
d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z"
fill="url(#paint2_linear_415_2)"
/>
<mask
id="mask0_415_2"
style={{ maskType: "alpha" }}
maskUnits="userSpaceOnUse"
x="39"
y="16"
width="142"
height="189">
<path
d="M39.1602 147.335H95.8321V175.671C95.8321 191.32 83.1457 204.007 67.4962 204.007C51.8466 204.007 39.1602 191.32 39.1602 175.671V147.335Z"
fill="url(#paint3_linear_415_2)"
/>
<path
d="M39.1602 81.8081H152.504C168.154 81.8081 180.84 94.4946 180.84 110.144C180.84 125.794 168.154 138.48 152.504 138.48H39.1602V81.8081Z"
fill="url(#paint4_linear_415_2)"
/>
<path
d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z"
fill="url(#paint5_linear_415_2)"
/>
</mask>
<g mask="url(#mask0_415_2)">
<g filter="url(#filter0_d_415_2)">
<mask
id="mask1_415_2"
style={{ maskType: "alpha" }}
maskUnits="userSpaceOnUse"
x="39"
y="16"
width="142"
height="189">
<path
d="M39.1602 147.335H95.8321V175.671C95.8321 191.32 83.1457 204.007 67.4962 204.007C51.8466 204.007 39.1602 191.32 39.1602 175.671V147.335Z"
fill="black"
fill-opacity="0.1"
/>
<path
d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z"
fill="black"
fill-opacity="0.1"
/>
<path
d="M39.1602 81.8081H152.504C168.154 81.8081 180.84 94.4946 180.84 110.144C180.84 125.794 168.154 138.48 152.504 138.48H39.1602V81.8081Z"
fill="black"
fill-opacity="0.1"
/>
</mask>
<g mask="url(#mask1_415_2)">
<path
d="M42.1331 -32.5321C64.3329 -54.1986 120.626 -32.5321 120.626 -32.5321H42.1331C36.6806 -27.2105 33.2847 -19.2749 33.2847 -7.76218C33.2847 50.6243 96.5317 71.8561 96.5317 112.55C96.5317 152.386 35.9231 176.962 33.3678 231.092H120.626C120.626 231.092 33.2847 291.248 33.2847 234.631C33.2847 233.437 33.3128 232.258 33.3678 231.092H-5.11523L2.41417 -32.5321H42.1331Z"
fill="black"
fill-opacity="0.1"
/>
</g>
</g>
<g filter="url(#filter1_f_415_2)">
<circle cx="21.4498" cy="179.212" r="53.13" fill="#00C4B8" />
</g>
<g filter="url(#filter2_f_415_2)">
<circle cx="21.4498" cy="44.6163" r="53.13" fill="#00C4B8" />
</g>
</g>
<defs>
<filter
id="filter0_d_415_2"
x="34.5149"
y="-11.5917"
width="137.209"
height="243.47"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feOffset dx="23.2262" />
<feGaussianBlur stdDeviation="13.9357" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_415_2" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_415_2" result="shape" />
</filter>
<filter
id="filter1_f_415_2"
x="-78.1326"
y="79.6296"
width="199.165"
height="199.165"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="23.2262" result="effect1_foregroundBlur_415_2" />
</filter>
<filter
id="filter2_f_415_2"
x="-78.1326"
y="-54.9661"
width="199.165"
height="199.165"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur stdDeviation="23.2262" result="effect1_foregroundBlur_415_2" />
</filter>
<linearGradient
id="paint0_linear_415_2"
x1="96.0786"
y1="174.643"
x2="39.1553"
y2="174.873"
gradientUnits="userSpaceOnUse">
<stop offset="1" stop-color="#00C4B8" />
</linearGradient>
<linearGradient
id="paint1_linear_415_2"
x1="181.456"
y1="109.116"
x2="39.1602"
y2="110.554"
gradientUnits="userSpaceOnUse">
<stop stop-color="#00DDD0" />
<stop offset="1" stop-color="#01E0C6" />
</linearGradient>
<linearGradient
id="paint2_linear_415_2"
x1="181.456"
y1="43.5891"
x2="39.1602"
y2="45.0264"
gradientUnits="userSpaceOnUse">
<stop stop-color="#00DDD0" />
<stop offset="1" stop-color="#01E0C6" />
</linearGradient>
<linearGradient
id="paint3_linear_415_2"
x1="96.0786"
y1="174.644"
x2="39.1553"
y2="174.874"
gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1" />
<stop offset="1" stop-color="#01E0C6" />
</linearGradient>
<linearGradient
id="paint4_linear_415_2"
x1="181.456"
y1="109.117"
x2="39.1602"
y2="110.555"
gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1" />
<stop offset="1" stop-color="#01E0C6" />
</linearGradient>
<linearGradient
id="paint5_linear_415_2"
x1="181.456"
y1="43.5891"
x2="39.1602"
y2="45.0264"
gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1" />
<stop offset="1" stop-color="#01E0C6" />
</linearGradient>
</defs>
</svg>
);
};