diff --git a/apps/docs/app/developer-docs/integrations/google-sheets/link-survey-with-sheet.webp b/apps/docs/app/developer-docs/integrations/google-sheets/link-survey-with-sheet.webp index 90e1e02d7b..daa7a1ee90 100644 Binary files a/apps/docs/app/developer-docs/integrations/google-sheets/link-survey-with-sheet.webp and b/apps/docs/app/developer-docs/integrations/google-sheets/link-survey-with-sheet.webp differ diff --git a/apps/docs/app/developer-docs/integrations/google-sheets/link-with-questions.webp b/apps/docs/app/developer-docs/integrations/google-sheets/link-with-questions.webp index 767a2f4143..c09b0656d0 100644 Binary files a/apps/docs/app/developer-docs/integrations/google-sheets/link-with-questions.webp and b/apps/docs/app/developer-docs/integrations/google-sheets/link-with-questions.webp differ diff --git a/apps/docs/app/developer-docs/integrations/google-sheets/page.mdx b/apps/docs/app/developer-docs/integrations/google-sheets/page.mdx index 5b3a934e33..f02a163a16 100644 --- a/apps/docs/app/developer-docs/integrations/google-sheets/page.mdx +++ b/apps/docs/app/developer-docs/integrations/google-sheets/page.mdx @@ -21,7 +21,8 @@ export const metadata = { The Google Sheets integration allows you to automatically send responses to a Google Sheet of your choice. - 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. ## 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. We store as little personal information as possible. diff --git a/apps/docs/app/self-hosting/integrations/page.mdx b/apps/docs/app/self-hosting/integrations/page.mdx index 4122e9a5e0..e88b3ff7d8 100644 --- a/apps/docs/app/self-hosting/integrations/page.mdx +++ b/apps/docs/app/self-hosting/integrations/page.mdx @@ -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:///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: diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal.tsx index fdfd65c664..66cca1115b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal.tsx @@ -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; - isLoading: boolean; - fetchTable: (val: string) => Promise; - airtableArray: TIntegrationItem[]; - setValue: UseFormSetValue; - defaultValue: string | undefined; -} - -const BaseSelect = ({ +export const AddIntegrationModal = ({ + open, + setOpenWithStates, + environmentId, airtableArray, - control, - fetchTable, - isLoading, - setValue, - defaultValue, -}: BaseSelectProps) => { - return ( -
- -
- ( - - )} - /> -
-
- ); -}; - -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([]); const [isLoading, setIsLoading] = useState(false); @@ -248,7 +195,7 @@ export const AddIntegrationModal = (props: AddIntegrationModalProps) => {
{airtableArray.length ? ( - { - 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 ? ( - ) : ( - + ); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown.tsx new file mode 100644 index 0000000000..ad8778f7b8 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown.tsx @@ -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; + isLoading: boolean; + fetchTable: (val: string) => Promise; + airtableArray: TIntegrationItem[]; + setValue: UseFormSetValue; + defaultValue: string | undefined; +} + +export const BaseSelectDropdown = ({ + airtableArray, + control, + fetchTable, + isLoading, + setValue, + defaultValue, +}: BaseSelectProps) => { + return ( +
+ +
+ ( + + )} + /> +
+
+ ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/Connect.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/Connect.tsx deleted file mode 100644 index a8b89728f2..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/Connect.tsx +++ /dev/null @@ -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 ( -
-
-
-
- Formbricks Logo -
-
- Airtable Logo -
-
-

Sync responses directly with Airtable.

- {!enabled && ( -

- Airtable Integration is not configured in your instance of Formbricks. -
- Please follow the{" "} - - docs - {" "} - to configure it. -

- )} - -
-
- ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/Home.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration.tsx similarity index 98% rename from apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/Home.tsx rename to apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration.tsx index 893dcecf11..aa55120b6e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/Home.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration.tsx @@ -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); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx index cedb32666c..3dbf70944f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx @@ -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 }) => {
{ +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); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx index 043dd9433e..fa58755fd6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/AddIntegrationModal.tsx @@ -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([]); const [isLinkingSheet, setIsLinkingSheet] = useState(false); const [selectedSurvey, setSelectedSurvey] = useState(null); - const [selectedSpreadsheet, setSelectedSpreadsheet] = useState(null); + const [spreadsheetUrl, setSpreadsheetUrl] = useState(""); const [isDeleting, setIsDeleting] = useState(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 ( - +
@@ -194,23 +191,13 @@ export const AddIntegrationModal = ({
- Spreadsheet URL + setSpreadsheetUrl(e.target.value)} + placeholder="https://docs.google.com/spreadsheets/d/" + className="mt-1" /> - {selectedSpreadsheet && hasMatchingId && ( -

- Warning: You have already connected one survey with this sheet. Your - data will be inconsistent -

- )} -

- {spreadsheets.length === 0 && - "You have to create at least one spreadshseet to be able to setup this integration"} -

{ - const [isConnecting, setIsConnecting] = useState(false); - const handleGoogleLogin = async () => { - setIsConnecting(true); - authorize(environmentId, webAppUrl).then((url: string) => { - if (url) { - window.location.replace(url); - } - }); - }; - - return ( -
-
-
-
- Formbricks Logo -
-
- Google Sheet logo -
-
-

Sync responses directly with Google Sheets.

- {!enabled && ( -

- Google Sheets Integration is not configured in your instance of Formbricks. -
- Please follow the{" "} - - docs - {" "} - to configure it. -

- )} - -
-
- ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper.tsx index 87bfc9e414..32022249c8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper.tsx @@ -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(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} /> - ) : ( - + )} ); diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/Home.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration.tsx similarity index 95% rename from apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/Home.tsx rename to apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration.tsx index e71d8f3f8b..2896762472 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/Home.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/components/ManageIntegration.tsx @@ -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 = ({ -
-
- ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/Home.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.tsx similarity index 95% rename from apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/Home.tsx rename to apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.tsx index 072166b50e..0b7955f2e3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/Home.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration.tsx @@ -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>; @@ -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 diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx index 2dae492c02..0de86aecdf 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper.tsx @@ -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 ? ( <> - ) : ( - + ); }; diff --git a/apps/web/app/api/google-sheet/route.ts b/apps/web/app/api/google-sheet/route.ts index e40b600f5e..0edc9b7283 100644 --- a/apps/web/app/api/google-sheet/route.ts +++ b/apps/web/app/api/google-sheet/route.ts @@ -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", ]; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/images/airtable.svg b/apps/web/images/airtableLogo.svg similarity index 100% rename from apps/web/app/(app)/environments/[environmentId]/integrations/airtable/images/airtable.svg rename to apps/web/images/airtableLogo.svg diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/images/google-sheets-small.png b/apps/web/images/googleSheetsLogo.png similarity index 100% rename from apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/images/google-sheets-small.png rename to apps/web/images/googleSheetsLogo.png diff --git a/packages/lib/googleSheet/service.ts b/packages/lib/googleSheet/service.ts index 1a7584ee22..601163701c 100644 --- a/packages/lib/googleSheet/service.ts +++ b/packages/lib/googleSheet/service.ts @@ -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 => { - 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 => { + 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; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/Connect.tsx b/packages/ui/ConnectIntegration/index.tsx similarity index 50% rename from apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/Connect.tsx rename to packages/ui/ConnectIntegration/index.tsx index 1206678cb8..0a7e8742f0 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/components/Connect.tsx +++ b/packages/ui/ConnectIntegration/index.tsx @@ -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 (
-
- Formbricks Logo +
+
- Google Sheet logo + logo
-

Sync responses directly with your Notion database.

- {!enabled && ( +

{integrationDetails?.text}

+ {!isEnabled && (

- Notion Integration is not configured in your instance of Formbricks. + {integrationDetails?.notConfiguredText}
Please follow the{" "} - + docs {" "} to configure it.

)} -
diff --git a/packages/ui/ConnectIntegration/lib/utils.ts b/packages/ui/ConnectIntegration/lib/utils.ts new file mode 100644 index 0000000000..5312b71941 --- /dev/null +++ b/packages/ui/ConnectIntegration/lib/utils.ts @@ -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.", + }; + } +}; diff --git a/packages/ui/FormbricksLogo/index.tsx b/packages/ui/FormbricksLogo/index.tsx new file mode 100644 index 0000000000..df5db6f379 --- /dev/null +++ b/packages/ui/FormbricksLogo/index.tsx @@ -0,0 +1,187 @@ +export const FormbricksLogo = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +};