Compare commits

...

13 Commits

Author SHA1 Message Date
Piyush Gupta
65e30f3ab0 fix: improve error handling for Slack integration and update error types 2024-11-11 14:44:27 +05:30
Piyush Gupta
7f1e3502e2 Merge branch 'main' of https://github.com/formbricks/formbricks into mertergolu-fix 2024-11-11 14:07:34 +05:30
Dhruwang Jariwala
be3bbdf2e2 Merge branch 'main' into main 2024-11-08 16:32:38 +05:30
Dhruwang
b1c2f2c4cd fix reconnect 2024-11-08 16:31:50 +05:30
Dhruwang
7616133a25 tweaks 2024-11-08 15:18:33 +05:30
Dhruwang
60139afd81 merged main 2024-11-08 15:14:05 +05:30
Dhruwang
19e5865d05 fix: backward compatibility 2024-10-17 12:08:53 +05:30
Dhruwang
6c5c27f571 Merge branch 'main' of https://github.com/merteroglu/formbricks into merteroglu/main 2024-10-16 17:48:30 +05:30
Dhruwang Jariwala
c12fb1a9f8 Merge branch 'main' into main 2024-10-16 17:46:00 +05:30
Dhruwang
526439def3 Merge branch 'main' of https://github.com/merteroglu/formbricks into merteroglu/main 2024-10-04 11:45:19 +05:30
Dhruwang
4e01ac211f Merge branch 'main' of https://github.com/Dhruwang/formbricks into merteroglu/main 2024-10-04 11:44:48 +05:30
Dhruwang Jariwala
f2f3ff6d46 Merge branch 'main' into main 2024-10-04 11:44:41 +05:30
Mert Eroğlu
b332cf12ca implement new slack ep 2024-09-29 23:21:30 +03:00
18 changed files with 104 additions and 39 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -8,7 +8,10 @@ import LinkWithQuestions from "./images/link-with-questions.webp";
import ListLinkedSurveys from "./images/list-linked-surveys.webp";
import SlackAuth from "./images/slack-auth.webp";
import SlackConnected from "./images/slack-connected.webp";
import AddSlackBot1 from "./images/add-slack-bot-1.webp";
import AddSlackBot2 from "./images/add-slack-bot-2.webp";
import AddSlackBot3 from "./images/add-slack-bot-3.webp";
import AddSlackBot4 from "./images/add-slack-bot-4.webp";
export const metadata = {
title: "Slack",
description:
@@ -69,7 +72,34 @@ The slack integration allows you to automatically send responses to a Slack chan
channel in the Slack workspace you integrated.
</Note>
5. Now click on the "Link channel" button to link a Slack channel with Formbricks and a modal will open up.
5. In order to make your channel available in channel dropdown, you need to add formbricks integration bot to the channel you want to link. You can do this by going to channel settings -> Integrations -> Add apps -> Search for "Formbricks" -> Select the bot -> Add.
<MdxImage
src={AddSlackBot1}
alt="Click on three dot at top right of the channel"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<MdxImage
src={AddSlackBot2}
alt="Select Edit Settings"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<MdxImage
src={AddSlackBot3}
alt="Navigate to Integrations"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<MdxImage
src={AddSlackBot4}
alt="Add Formbricks Bot"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
6. Now click on the "Link channel" button to link a Slack channel with Formbricks and a modal will open up.
<MdxImage
src={LinkSurveyWithChannel}
@@ -78,7 +108,7 @@ The slack integration allows you to automatically send responses to a Slack chan
className="max-w-full rounded-lg sm:max-w-3xl"
/>
6. Select the channel you want to link with Formbricks and the Survey. On doing so, you will be asked to select the questions' responses you want to feed in the Slack channel. Select the questions and click on the "Link Channel" button.
7. Select the channel you want to link with Formbricks and the Survey. On doing so, you will be asked to select the questions' responses you want to feed in the Slack channel. Select the questions and click on the "Link Channel" button.
<MdxImage
src={LinkWithQuestions}
@@ -87,7 +117,7 @@ The slack integration allows you to automatically send responses to a Slack chan
className="max-w-full rounded-lg sm:max-w-3xl"
/>
7. On submitting, the modal will close and you will see the linked Slack channel in the list of linked Slack channels.
8. On submitting, the modal will close and you will see the linked Slack channel in the list of linked Slack channels.
<MdxImage
src={ListLinkedSurveys}
@@ -124,6 +154,7 @@ Enabling the Slack Integration in a self-hosted environment requires a setup usi
4. Go to the **OAuth & Permissions** tab on the sidebar and add the following **Bot Token Scopes**:
- `channels:read`
- `groups:read`
- `chat:write`
- `chat:write.public`
- `chat:write.customize`

View File

@@ -302,6 +302,7 @@ Enabling the Slack Integration in a self-hosted environment requires a setup usi
4. Go to the **OAuth & Permissions** tab on the sidebar and add the following **Bot Token Scopes**:
- `channels:read`
- `groups:read`
- `chat:write`
- `chat:write.public`
- `chat:write.customize`

View File

@@ -337,7 +337,7 @@ export const SurveyMenuBar = ({
/>
</div>
{responseCount > 0 && (
<div className="ju flex items-center rounded-lg border border-amber-200 bg-amber-100 p-1.5 text-amber-800 shadow-sm lg:mx-auto">
<div className="flex items-center rounded-lg border border-amber-200 bg-amber-100 p-1.5 text-amber-800 shadow-sm lg:mx-auto">
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>

View File

@@ -7,12 +7,12 @@ import { z } from "zod";
import { getSlackChannels } from "@formbricks/lib/slack/service";
import { ZId } from "@formbricks/types/common";
const ZRefreshChannelsAction = z.object({
const ZGetSlackChannelsAction = z.object({
environmentId: ZId,
});
export const refreshChannelsAction = authenticatedActionClient
.schema(ZRefreshChannelsAction)
export const getSlackChannelsAction = authenticatedActionClient
.schema(ZGetSlackChannelsAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,

View File

@@ -23,6 +23,8 @@ interface ManageIntegrationProps {
React.SetStateAction<(TIntegrationSlackConfigData & { index: number }) | null>
>;
refreshChannels: () => void;
showReconnectButton: boolean;
handleSlackAuthorization: () => void;
locale: TUserLocale;
}
@@ -33,6 +35,8 @@ export const ManageIntegration = ({
setIsConnected,
setSelectedIntegration,
refreshChannels,
showReconnectButton,
handleSlackAuthorization,
locale,
}: ManageIntegrationProps) => {
const t = useTranslations();
@@ -70,7 +74,19 @@ export const ManageIntegration = ({
return (
<div className="mt-6 flex w-full flex-col items-center justify-center p-6">
<div className="flex w-full justify-end">
{showReconnectButton && (
<div className="mb-4 flex w-full items-center justify-between space-x-4">
<p className="text-amber-700">
{t.rich("environments.integrations.slack.slack_reconnect_button_description", {
b: (chunks) => <b>{chunks}</b>,
})}
</p>
<Button onClick={handleSlackAuthorization} variant="secondary">
{t("environments.integrations.slack.slack_reconnect_button")}
</Button>
</div>
)}
<div className="flex w-full justify-end space-x-4">
<div className="mr-6 flex items-center">
<span className="mr-4 h-4 w-4 rounded-full bg-green-600"></span>
<span className="text-slate-500">

View File

@@ -1,11 +1,11 @@
"use client";
import { refreshChannelsAction } from "@/app/(app)/environments/[environmentId]/integrations/slack/actions";
import { getSlackChannelsAction } from "@/app/(app)/environments/[environmentId]/integrations/slack/actions";
import { AddChannelMappingModal } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/AddChannelMappingModal";
import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/ManageIntegration";
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/slack/lib/slack";
import slackLogo from "@/images/slacklogo.png";
import { useState } from "react";
import { useEffect, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationItem } from "@formbricks/types/integration";
@@ -18,7 +18,6 @@ interface SlackWrapperProps {
isEnabled: boolean;
environment: TEnvironment;
surveys: TSurvey[];
channelsArray: TIntegrationItem[];
slackIntegration?: TIntegrationSlack;
webAppUrl: string;
attributeClasses: TAttributeClass[];
@@ -29,27 +28,38 @@ export const SlackWrapper = ({
isEnabled,
environment,
surveys,
channelsArray,
slackIntegration,
webAppUrl,
attributeClasses,
locale,
}: SlackWrapperProps) => {
const [isConnected, setIsConnected] = useState(slackIntegration ? slackIntegration.config?.key : false);
const [slackChannels, setSlackChannels] = useState(channelsArray);
const [slackChannels, setSlackChannels] = useState<TIntegrationItem[]>([]);
const [isModalOpen, setModalOpen] = useState<boolean>(false);
const [showReconnectButton, setShowReconnectButton] = useState<boolean>(false);
const [selectedIntegration, setSelectedIntegration] = useState<
(TIntegrationSlackConfigData & { index: number }) | null
>(null);
const refreshChannels = async () => {
const refreshChannelsResponse = await refreshChannelsAction({ environmentId: environment.id });
const getSlackChannels = async () => {
const getSlackChannelsResponse = await getSlackChannelsAction({ environmentId: environment.id });
if (refreshChannelsResponse?.data) {
setSlackChannels(refreshChannelsResponse.data);
if (
getSlackChannelsResponse?.serverError &&
getSlackChannelsResponse.serverError.includes("missing_scope")
) {
setShowReconnectButton(true);
}
if (getSlackChannelsResponse?.data) {
setSlackChannels(getSlackChannelsResponse.data);
}
};
useEffect(() => {
getSlackChannels();
}, []);
const handleSlackAuthorization = async () => {
authorize(environment.id, webAppUrl).then((url: string) => {
if (url) {
@@ -76,7 +86,9 @@ export const SlackWrapper = ({
setOpenAddIntegrationModal={setModalOpen}
setIsConnected={setIsConnected}
setSelectedIntegration={setSelectedIntegration}
refreshChannels={refreshChannels}
refreshChannels={getSlackChannels}
showReconnectButton={showReconnectButton}
handleSlackAuthorization={handleSlackAuthorization}
locale={locale}
/>
</>

View File

@@ -12,10 +12,8 @@ import { getIntegrationByType } from "@formbricks/lib/integration/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getSlackChannels } from "@formbricks/lib/slack/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
import { GoBackButton } from "@formbricks/ui/components/GoBackButton";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
@@ -45,10 +43,6 @@ const Page = async ({ params }) => {
throw new Error(t("common.product_not_found"));
}
let channelsArray: TIntegrationItem[] = [];
if (slackIntegration && slackIntegration.config.key) {
channelsArray = await getSlackChannels(params.environmentId);
}
const locale = await findMatchingLocale();
const currentUserMembership = await getMembershipByUserIdOrganizationId(
@@ -75,7 +69,6 @@ const Page = async ({ params }) => {
<SlackWrapper
isEnabled={isEnabled}
environment={environment}
channelsArray={channelsArray}
surveys={surveys}
slackIntegration={slackIntegration as TIntegrationSlack}
webAppUrl={WEBAPP_URL}

View File

@@ -1,8 +1,12 @@
import { responses } from "@/app/lib/api/response";
import { NextRequest } from "next/server";
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import { createOrUpdateIntegration } from "@formbricks/lib/integration/service";
import { TIntegrationSlackConfig, TIntegrationSlackCredential } from "@formbricks/types/integration/slack";
import { createOrUpdateIntegration, getIntegrationByType } from "@formbricks/lib/integration/service";
import {
TIntegrationSlackConfig,
TIntegrationSlackConfigData,
TIntegrationSlackCredential,
} from "@formbricks/types/integration/slack";
export const GET = async (req: NextRequest) => {
const url = req.url;
@@ -58,18 +62,20 @@ export const GET = async (req: NextRequest) => {
team: data.team,
};
const slackIntegration = await getIntegrationByType(environmentId, "slack");
const slackConfiguration: TIntegrationSlackConfig = {
data: [],
data: (slackIntegration?.config.data as TIntegrationSlackConfigData[]) ?? [],
key: slackCredentials,
};
const slackIntegration = {
const integration = {
type: "slack" as "slack",
environment: environmentId,
config: slackConfiguration,
};
const result = await createOrUpdateIntegration(environmentId, slackIntegration);
const result = await createOrUpdateIntegration(environmentId, integration);
if (result) {
return Response.redirect(`${WEBAPP_URL}/environments/${environmentId}/integrations/slack`);

View File

@@ -22,7 +22,6 @@ export const actionClient = createSafeActionClient({
}
console.error("SERVER ERROR: ", e);
return DEFAULT_SERVER_ERROR_MESSAGE;
},
});

View File

@@ -53,7 +53,7 @@ export const INVITE_DISABLED = env.INVITE_DISABLED === "1";
export const SLACK_CLIENT_SECRET = env.SLACK_CLIENT_SECRET;
export const SLACK_CLIENT_ID = env.SLACK_CLIENT_ID;
export const SLACK_AUTH_URL = `https://slack.com/oauth/v2/authorize?client_id=${env.SLACK_CLIENT_ID}&scope=channels:read,chat:write,chat:write.public,chat:write.customize`;
export const SLACK_AUTH_URL = `https://slack.com/oauth/v2/authorize?client_id=${env.SLACK_CLIENT_ID}&scope=channels:read,chat:write,chat:write.public,chat:write.customize,groups:read`;
export const GOOGLE_SHEETS_CLIENT_ID = env.GOOGLE_SHEETS_CLIENT_ID;
export const GOOGLE_SHEETS_CLIENT_SECRET = env.GOOGLE_SHEETS_CLIENT_SECRET;

View File

@@ -685,7 +685,9 @@
"select_channel": "Kanal auswählen",
"slack_integration": "Slack Integration",
"slack_integration_description": "Sende Antworten direkt an Slack.",
"slack_integration_is_not_configured": "Slack Integration ist in deiner Instanz von Formbricks nicht konfiguriert."
"slack_integration_is_not_configured": "Slack Integration ist in deiner Instanz von Formbricks nicht konfiguriert.",
"slack_reconnect_button": "Erneut verbinden",
"slack_reconnect_button_description": "<b>Hinweis:</b> Wir haben kürzlich unsere Slack-Integration geändert, um auch private Kanäle zu unterstützen. Bitte verbinden Sie Ihren Slack-Workspace erneut."
},
"slack_integration_description": "Verbinde deinen Slack Arbeitsbereich sofort mit Formbricks",
"to_configure_it": "es zu konfigurieren.",

View File

@@ -685,7 +685,9 @@
"select_channel": "Select Channel",
"slack_integration": "Slack Integration",
"slack_integration_description": "Send responses directly to Slack.",
"slack_integration_is_not_configured": "Slack Integration is not configured in your instance of Formbricks."
"slack_integration_is_not_configured": "Slack Integration is not configured in your instance of Formbricks.",
"slack_reconnect_button": "Reconnect",
"slack_reconnect_button_description": "<b>Note:</b> We recently changed our Slack integration to also support private channels. Please reconnect your Slack workspace."
},
"slack_integration_description": "Instantly connect your Slack Workspace with Formbricks",
"to_configure_it": "to configure it.",

View File

@@ -685,7 +685,9 @@
"select_channel": "Selecionar Canal",
"slack_integration": "Integração com o Slack",
"slack_integration_description": "Manda as respostas direto pro Slack.",
"slack_integration_is_not_configured": "A integração do Slack não está configurada na sua instância do Formbricks."
"slack_integration_is_not_configured": "A integração do Slack não está configurada na sua instância do Formbricks.",
"slack_reconnect_button": "Reconectar",
"slack_reconnect_button_description": "<b>Observação:</b> Recentemente, alteramos nossa integração com o Slack para também suportar canais privados. Por favor, reconecte seu workspace do Slack."
},
"slack_integration_description": "Conecte instantaneamente seu Workspace do Slack com o Formbricks",
"to_configure_it": "configurar isso.",

View File

@@ -1,5 +1,5 @@
import { Prisma } from "@prisma/client";
import { DatabaseError } from "@formbricks/types/errors";
import { DatabaseError, UnknownError } from "@formbricks/types/errors";
import { TIntegration, TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationSlack, TIntegrationSlackCredential } from "@formbricks/types/integration/slack";
import { deleteIntegration, getIntegrationByType } from "../integration/service";
@@ -11,8 +11,9 @@ export const fetchChannels = async (slackIntegration: TIntegration): Promise<TIn
let nextCursor: string | undefined = undefined;
do {
const url = new URL("https://slack.com/api/conversations.list");
const url = new URL("https://slack.com/api/users.conversations");
url.searchParams.append("limit", "200");
url.searchParams.append("types", "private_channel,public_channel");
if (nextCursor) {
url.searchParams.append("cursor", nextCursor);
}
@@ -65,7 +66,7 @@ export const getSlackChannels = async (environmentId: string): Promise<TIntegrat
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
throw new UnknownError(error);
}
};