mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-25 16:00:16 -06:00
Compare commits
13 Commits
fix/block-
...
mertergolu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65e30f3ab0 | ||
|
|
7f1e3502e2 | ||
|
|
be3bbdf2e2 | ||
|
|
b1c2f2c4cd | ||
|
|
7616133a25 | ||
|
|
60139afd81 | ||
|
|
19e5865d05 | ||
|
|
6c5c27f571 | ||
|
|
c12fb1a9f8 | ||
|
|
526439def3 | ||
|
|
4e01ac211f | ||
|
|
f2f3ff6d46 | ||
|
|
b332cf12ca |
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 |
@@ -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`
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -22,7 +22,6 @@ export const actionClient = createSafeActionClient({
|
||||
}
|
||||
|
||||
console.error("SERVER ERROR: ", e);
|
||||
|
||||
return DEFAULT_SERVER_ERROR_MESSAGE;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user