feat: Add event selection to Slack post integration (#2857)
This commit is contained in:
@@ -2,13 +2,15 @@ import * as React from "react";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ButtonLink(props: Props) {
|
const ButtonLink = React.forwardRef(
|
||||||
return <Button {...props} />;
|
(props: Props, ref: React.Ref<HTMLButtonElement>) => {
|
||||||
}
|
return <Button {...props} ref={ref} />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const Button = styled.button`
|
const Button = styled.button`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -20,3 +22,5 @@ const Button = styled.button`
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export default ButtonLink;
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ export default class BaseModel {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// ensure that the id is passed if the document has one
|
// ensure that the id is passed if the document has one
|
||||||
if (params) {
|
if (!params) {
|
||||||
params = { ...params, id: this.id };
|
params = this.toJS();
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = await this.store.save(params || this.toJS());
|
const model = await this.store.save({ ...params, id: this.id });
|
||||||
|
|
||||||
// if saving is successful set the new values on the model itself
|
// if saving is successful set the new values on the model itself
|
||||||
set(this, { ...params, ...model });
|
set(this, { ...params, ...model });
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { extendObservable, action } from "mobx";
|
import { observable } from "mobx";
|
||||||
import BaseModel from "~/models/BaseModel";
|
import BaseModel from "~/models/BaseModel";
|
||||||
import { client } from "~/utils/ApiClient";
|
import Field from "./decorators/Field";
|
||||||
|
|
||||||
type Settings = {
|
type Settings = {
|
||||||
url: string;
|
url: string;
|
||||||
channel: string;
|
channel: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
};
|
};
|
||||||
type Events = "documents.create" | "collections.create";
|
|
||||||
|
|
||||||
class Integration extends BaseModel {
|
class Integration extends BaseModel {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,24 +17,11 @@ class Integration extends BaseModel {
|
|||||||
|
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
|
|
||||||
events: Events;
|
@Field
|
||||||
|
@observable
|
||||||
|
events: string[];
|
||||||
|
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
|
|
||||||
@action
|
|
||||||
update = async (data: Record<string, any>) => {
|
|
||||||
await client.post("/integrations.update", {
|
|
||||||
id: this.id,
|
|
||||||
...data,
|
|
||||||
});
|
|
||||||
extendObservable(this, data);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
@action
|
|
||||||
delete = () => {
|
|
||||||
return this.store.delete(this);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Integration;
|
export default Integration;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as React from "react";
|
|||||||
import { useTranslation, Trans } from "react-i18next";
|
import { useTranslation, Trans } from "react-i18next";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Collection from "~/models/Collection";
|
import Collection from "~/models/Collection";
|
||||||
|
import Integration from "~/models/Integration";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import CollectionIcon from "~/components/CollectionIcon";
|
import CollectionIcon from "~/components/CollectionIcon";
|
||||||
import Heading from "~/components/Heading";
|
import Heading from "~/components/Heading";
|
||||||
@@ -18,6 +19,7 @@ import useCurrentTeam from "~/hooks/useCurrentTeam";
|
|||||||
import useQuery from "~/hooks/useQuery";
|
import useQuery from "~/hooks/useQuery";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import SlackButton from "./components/SlackButton";
|
import SlackButton from "./components/SlackButton";
|
||||||
|
import SlackListItem from "./components/SlackListItem";
|
||||||
|
|
||||||
function Slack() {
|
function Slack() {
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
@@ -40,6 +42,16 @@ function Slack() {
|
|||||||
(i) => i.type === "command"
|
(i) => i.type === "command"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const groupedCollections = collections.orderedData
|
||||||
|
.map<[Collection, Integration | undefined]>((collection) => {
|
||||||
|
const integration = find(integrations.slackIntegrations, {
|
||||||
|
collectionId: collection.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [collection, integration];
|
||||||
|
})
|
||||||
|
.sort((a) => (a[1] ? -1 : 1));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scene title="Slack" icon={<SlackIcon color="currentColor" />}>
|
<Scene title="Slack" icon={<SlackIcon color="currentColor" />}>
|
||||||
<Heading>Slack</Heading>
|
<Heading>Slack</Heading>
|
||||||
@@ -83,6 +95,7 @@ function Slack() {
|
|||||||
scopes={["commands", "links:read", "links:write"]}
|
scopes={["commands", "links:read", "links:write"]}
|
||||||
redirectUri={`${env.URL}/auth/slack.commands`}
|
redirectUri={`${env.URL}/auth/slack.commands`}
|
||||||
state={team.id}
|
state={team.id}
|
||||||
|
icon={<SlackIcon color="currentColor" />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
@@ -98,33 +111,13 @@ function Slack() {
|
|||||||
</HelpText>
|
</HelpText>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
{collections.orderedData.map((collection: Collection) => {
|
{groupedCollections.map(([collection, integration]) => {
|
||||||
const integration = find(integrations.slackIntegrations, {
|
|
||||||
collectionId: collection.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (integration) {
|
if (integration) {
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<SlackListItem
|
||||||
key={integration.id}
|
key={integration.id}
|
||||||
title={collection.name}
|
collection={collection}
|
||||||
image={<CollectionIcon collection={collection} />}
|
integration={integration}
|
||||||
subtitle={
|
|
||||||
<Trans
|
|
||||||
defaults={`Connected to the <em>{{ channelName }}</em> channel`}
|
|
||||||
values={{
|
|
||||||
channelName: integration.settings.channel,
|
|
||||||
}}
|
|
||||||
components={{
|
|
||||||
em: <strong />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<Button onClick={integration.delete} neutral>
|
|
||||||
{t("Disconnect")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ import * as React from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { slackAuth } from "@shared/utils/routeHelpers";
|
import { slackAuth } from "@shared/utils/routeHelpers";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import SlackIcon from "~/components/SlackIcon";
|
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
state?: string;
|
state?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function SlackButton({ state = "", scopes, redirectUri, label }: Props) {
|
function SlackButton({ state = "", scopes, redirectUri, label, icon }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleClick = () =>
|
const handleClick = () =>
|
||||||
@@ -24,11 +24,7 @@ function SlackButton({ state = "", scopes, redirectUri, label }: Props) {
|
|||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button onClick={handleClick} icon={icon} neutral>
|
||||||
onClick={handleClick}
|
|
||||||
icon={<SlackIcon color="currentColor" />}
|
|
||||||
neutral
|
|
||||||
>
|
|
||||||
{label || t("Add to Slack")}
|
{label || t("Add to Slack")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
115
app/scenes/Settings/components/SlackListItem.tsx
Normal file
115
app/scenes/Settings/components/SlackListItem.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { uniq } from "lodash";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import Collection from "~/models/Collection";
|
||||||
|
import Integration from "~/models/Integration";
|
||||||
|
import Button from "~/components/Button";
|
||||||
|
import ButtonLink from "~/components/ButtonLink";
|
||||||
|
import Checkbox from "~/components/Checkbox";
|
||||||
|
import CollectionIcon from "~/components/CollectionIcon";
|
||||||
|
import Flex from "~/components/Flex";
|
||||||
|
import HelpText from "~/components/HelpText";
|
||||||
|
import ListItem from "~/components/List/Item";
|
||||||
|
import Popover from "~/components/Popover";
|
||||||
|
import useToasts from "~/hooks/useToasts";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
integration: Integration;
|
||||||
|
collection: Collection;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SlackListItem({ integration, collection }: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { showToast } = useToasts();
|
||||||
|
|
||||||
|
const handleChange = async (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (ev.target.checked) {
|
||||||
|
integration.events = uniq([...integration.events, ev.target.name]);
|
||||||
|
} else {
|
||||||
|
integration.events = integration.events.filter(
|
||||||
|
(n) => n !== ev.target.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await integration.save();
|
||||||
|
|
||||||
|
showToast(t("Settings saved"), {
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapping = {
|
||||||
|
"documents.publish": t("document published"),
|
||||||
|
"documents.update": t("document updated"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const popover = usePopoverState({
|
||||||
|
gutter: 0,
|
||||||
|
placement: "bottom-start",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={integration.id}
|
||||||
|
title={
|
||||||
|
<Flex align="center" gap={6}>
|
||||||
|
<CollectionIcon collection={collection} /> {collection.name}
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
subtitle={
|
||||||
|
<>
|
||||||
|
<Trans
|
||||||
|
defaults={`Posting to the <em>{{ channelName }}</em> channel on`}
|
||||||
|
values={{
|
||||||
|
channelName: integration.settings.channel,
|
||||||
|
events: integration.events.map((ev) => mapping[ev]).join(", "),
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
em: <strong />,
|
||||||
|
}}
|
||||||
|
/>{" "}
|
||||||
|
<PopoverDisclosure {...popover}>
|
||||||
|
{(props) => (
|
||||||
|
<ButtonLink {...props}>
|
||||||
|
{integration.events.map((ev) => mapping[ev]).join(", ")}
|
||||||
|
</ButtonLink>
|
||||||
|
)}
|
||||||
|
</PopoverDisclosure>
|
||||||
|
<Popover {...popover} aria-label={t("Settings")}>
|
||||||
|
<Events>
|
||||||
|
<h3>{t("Notifications")}</h3>
|
||||||
|
<HelpText>{t("These events should be posted to Slack")}</HelpText>
|
||||||
|
<Checkbox
|
||||||
|
label={t("Document published")}
|
||||||
|
name="documents.publish"
|
||||||
|
checked={integration.events.includes("documents.publish")}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t("Document updated")}
|
||||||
|
name="documents.update"
|
||||||
|
checked={integration.events.includes("documents.update")}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Events>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<Button onClick={integration.delete} neutral>
|
||||||
|
{t("Disconnect")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Events = styled.div`
|
||||||
|
color: ${(props) => props.theme.text};
|
||||||
|
margin-top: -12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default observer(SlackListItem);
|
||||||
@@ -17,8 +17,7 @@ export default async function documentUpdater({
|
|||||||
const document = await Document.findByPk(documentId);
|
const document = await Document.findByPk(documentId);
|
||||||
const state = Y.encodeStateAsUpdate(ydoc);
|
const state = Y.encodeStateAsUpdate(ydoc);
|
||||||
const node = Node.fromJSON(schema, yDocToProsemirrorJSON(ydoc, "default"));
|
const node = Node.fromJSON(schema, yDocToProsemirrorJSON(ydoc, "default"));
|
||||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
const text = serializer.serialize(node, undefined);
|
||||||
const text = serializer.serialize(node);
|
|
||||||
const isUnchanged = document.text === text;
|
const isUnchanged = document.text === text;
|
||||||
const hasMultiplayerState = !!document.state;
|
const hasMultiplayerState = !!document.state;
|
||||||
|
|
||||||
|
|||||||
18
server/migrations/20211217054419-integration-events.js
Normal file
18
server/migrations/20211217054419-integration-events.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
update integrations
|
||||||
|
set "events" = '{documents.update,documents.publish}'
|
||||||
|
where type = 'post'
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
update integrations
|
||||||
|
set "events" = NULL
|
||||||
|
where type = 'post'
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import fetch from "fetch-with-proxy";
|
import fetch from "fetch-with-proxy";
|
||||||
|
import { Op } from "sequelize";
|
||||||
import { Document, Integration, Collection, Team } from "@server/models";
|
import { Document, Integration, Collection, Team } from "@server/models";
|
||||||
import { presentSlackAttachment } from "@server/presenters";
|
import { presentSlackAttachment } from "@server/presenters";
|
||||||
import {
|
import {
|
||||||
@@ -38,8 +39,10 @@ export default class SlackProcessor {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
if (!integration) return;
|
if (!integration) return;
|
||||||
|
|
||||||
const collection = integration.collection;
|
const collection = integration.collection;
|
||||||
if (!collection) return;
|
if (!collection) return;
|
||||||
|
|
||||||
await fetch(integration.settings.url, {
|
await fetch(integration.settings.url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -68,14 +71,19 @@ export default class SlackProcessor {
|
|||||||
Team.findByPk(event.teamId),
|
Team.findByPk(event.teamId),
|
||||||
]);
|
]);
|
||||||
if (!document) return;
|
if (!document) return;
|
||||||
|
|
||||||
// never send notifications for draft documents
|
// never send notifications for draft documents
|
||||||
if (!document.publishedAt) return;
|
if (!document.publishedAt) return;
|
||||||
|
|
||||||
const integration = await Integration.findOne({
|
const integration = await Integration.findOne({
|
||||||
where: {
|
where: {
|
||||||
teamId: document.teamId,
|
teamId: document.teamId,
|
||||||
collectionId: document.collectionId,
|
collectionId: document.collectionId,
|
||||||
service: "slack",
|
service: "slack",
|
||||||
type: "post",
|
type: "post",
|
||||||
|
events: {
|
||||||
|
[Op.contains]: [event.name],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!integration) return;
|
if (!integration) return;
|
||||||
|
|||||||
53
server/routes/api/integrations.test.ts
Normal file
53
server/routes/api/integrations.test.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'fetc... Remove this comment to see the full error message
|
||||||
|
import TestServer from "fetch-test-server";
|
||||||
|
import webService from "@server/services/web";
|
||||||
|
import {
|
||||||
|
buildAdmin,
|
||||||
|
buildTeam,
|
||||||
|
buildUser,
|
||||||
|
buildIntegration,
|
||||||
|
} from "@server/test/factories";
|
||||||
|
import { flushdb } from "@server/test/support";
|
||||||
|
|
||||||
|
const app = webService();
|
||||||
|
const server = new TestServer(app.callback());
|
||||||
|
|
||||||
|
beforeEach(() => flushdb());
|
||||||
|
afterAll(() => server.close());
|
||||||
|
|
||||||
|
describe("#integrations.update", () => {
|
||||||
|
it("should allow updating integration events", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const user = await buildAdmin({ teamId: team.id });
|
||||||
|
const integration = await buildIntegration({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await server.post("/api/integrations.update", {
|
||||||
|
body: {
|
||||||
|
events: ["documents.update"],
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
id: integration.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.id).toEqual(integration.id);
|
||||||
|
expect(body.data.events.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should require authorization", async () => {
|
||||||
|
const user = await buildUser();
|
||||||
|
const integration = await buildIntegration({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
const res = await server.post("/api/integrations.update", {
|
||||||
|
body: {
|
||||||
|
token: user.getJwtToken(),
|
||||||
|
id: integration.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@ import { Event } from "@server/models";
|
|||||||
import Integration from "@server/models/Integration";
|
import Integration from "@server/models/Integration";
|
||||||
import policy from "@server/policies";
|
import policy from "@server/policies";
|
||||||
import { presentIntegration } from "@server/presenters";
|
import { presentIntegration } from "@server/presenters";
|
||||||
import { assertSort, assertUuid } from "@server/validation";
|
import { assertSort, assertUuid, assertArray } from "@server/validation";
|
||||||
import pagination from "./middlewares/pagination";
|
import pagination from "./middlewares/pagination";
|
||||||
|
|
||||||
const { authorize } = policy;
|
const { authorize } = policy;
|
||||||
@@ -31,13 +31,37 @@ router.post("integrations.list", auth(), pagination(), async (ctx) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post("integrations.update", auth(), async (ctx) => {
|
||||||
|
const { id, events } = ctx.body;
|
||||||
|
assertUuid(id, "id is required");
|
||||||
|
|
||||||
|
const { user } = ctx.state;
|
||||||
|
const integration = await Integration.findByPk(id);
|
||||||
|
authorize(user, "update", integration);
|
||||||
|
|
||||||
|
assertArray(events, "events must be an array");
|
||||||
|
|
||||||
|
if (integration.type === "post") {
|
||||||
|
integration.events = events.filter((event: string) =>
|
||||||
|
["documents.update", "documents.publish"].includes(event)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await integration.save();
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
data: presentIntegration(integration),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
router.post("integrations.delete", auth(), async (ctx) => {
|
router.post("integrations.delete", auth(), async (ctx) => {
|
||||||
const { id } = ctx.body;
|
const { id } = ctx.body;
|
||||||
assertUuid(id, "id is required");
|
assertUuid(id, "id is required");
|
||||||
|
|
||||||
const user = ctx.state.user;
|
const { user } = ctx.state;
|
||||||
const integration = await Integration.findByPk(id);
|
const integration = await Integration.findByPk(id);
|
||||||
authorize(user, "delete", integration);
|
authorize(user, "delete", integration);
|
||||||
|
|
||||||
await integration.destroy();
|
await integration.destroy();
|
||||||
await Event.create({
|
await Event.create({
|
||||||
name: "integrations.delete",
|
name: "integrations.delete",
|
||||||
@@ -46,6 +70,7 @@ router.post("integrations.delete", auth(), async (ctx) => {
|
|||||||
actorId: user.id,
|
actorId: user.id,
|
||||||
ip: ctx.request.ip,
|
ip: ctx.request.ip,
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ if (SLACK_CLIENT_ID) {
|
|||||||
const { code, error, state } = ctx.request.query;
|
const { code, error, state } = ctx.request.query;
|
||||||
const user = ctx.state.user;
|
const user = ctx.state.user;
|
||||||
assertPresent(code || error, "code is required");
|
assertPresent(code || error, "code is required");
|
||||||
|
|
||||||
const collectionId = state;
|
const collectionId = state;
|
||||||
assertUuid(collectionId, "collectionId must be an uuid");
|
assertUuid(collectionId, "collectionId must be an uuid");
|
||||||
|
|
||||||
@@ -179,8 +180,7 @@ if (SLACK_CLIENT_ID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = `${process.env.URL || ""}/auth/slack.post`;
|
const endpoint = `${process.env.URL || ""}/auth/slack.post`;
|
||||||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | string[] | undefined' i... Remove this comment to see the full error message
|
const data = await Slack.oauthAccess(code as string, endpoint);
|
||||||
const data = await Slack.oauthAccess(code, endpoint);
|
|
||||||
const authentication = await IntegrationAuthentication.create({
|
const authentication = await IntegrationAuthentication.create({
|
||||||
service: "slack",
|
service: "slack",
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -188,6 +188,7 @@ if (SLACK_CLIENT_ID) {
|
|||||||
token: data.access_token,
|
token: data.access_token,
|
||||||
scopes: data.scope.split(","),
|
scopes: data.scope.split(","),
|
||||||
});
|
});
|
||||||
|
|
||||||
await Integration.create({
|
await Integration.create({
|
||||||
service: "slack",
|
service: "slack",
|
||||||
type: "post",
|
type: "post",
|
||||||
@@ -195,7 +196,7 @@ if (SLACK_CLIENT_ID) {
|
|||||||
teamId: user.teamId,
|
teamId: user.teamId,
|
||||||
authenticationId: authentication.id,
|
authenticationId: authentication.id,
|
||||||
collectionId,
|
collectionId,
|
||||||
events: [],
|
events: ["documents.update", "documents.publish"],
|
||||||
settings: {
|
settings: {
|
||||||
url: data.incoming_webhook.url,
|
url: data.incoming_webhook.url,
|
||||||
channel: data.incoming_webhook.channel,
|
channel: data.incoming_webhook.channel,
|
||||||
|
|||||||
@@ -520,10 +520,17 @@
|
|||||||
"by {{ name }}": "by {{ name }}",
|
"by {{ name }}": "by {{ name }}",
|
||||||
"Last accessed": "Last accessed",
|
"Last accessed": "Last accessed",
|
||||||
"Add to Slack": "Add to Slack",
|
"Add to Slack": "Add to Slack",
|
||||||
|
"Settings saved": "Settings saved",
|
||||||
|
"document published": "document published",
|
||||||
|
"document updated": "document updated",
|
||||||
|
"Posting to the <em>{{ channelName }}</em> channel on": "Posting to the <em>{{ channelName }}</em> channel on",
|
||||||
|
"These events should be posted to Slack": "These events should be posted to Slack",
|
||||||
|
"Document published": "Document published",
|
||||||
|
"Document updated": "Document updated",
|
||||||
|
"Disconnect": "Disconnect",
|
||||||
"Active": "Active",
|
"Active": "Active",
|
||||||
"Everyone": "Everyone",
|
"Everyone": "Everyone",
|
||||||
"Admins": "Admins",
|
"Admins": "Admins",
|
||||||
"Settings saved": "Settings saved",
|
|
||||||
"Unable to upload new logo": "Unable to upload new logo",
|
"Unable to upload new logo": "Unable to upload new logo",
|
||||||
"These details affect the way that your Outline appears to everyone on the team.": "These details affect the way that your Outline appears to everyone on the team.",
|
"These details affect the way that your Outline appears to everyone on the team.": "These details affect the way that your Outline appears to everyone on the team.",
|
||||||
"Logo": "Logo",
|
"Logo": "Logo",
|
||||||
@@ -553,9 +560,7 @@
|
|||||||
"Requesting Export": "Requesting Export",
|
"Requesting Export": "Requesting Export",
|
||||||
"Export Data": "Export Data",
|
"Export Data": "Export Data",
|
||||||
"Recent exports": "Recent exports",
|
"Recent exports": "Recent exports",
|
||||||
"Document published": "Document published",
|
|
||||||
"Receive a notification whenever a new document is published": "Receive a notification whenever a new document is published",
|
"Receive a notification whenever a new document is published": "Receive a notification whenever a new document is published",
|
||||||
"Document updated": "Document updated",
|
|
||||||
"Receive a notification when a document you created is edited": "Receive a notification when a document you created is edited",
|
"Receive a notification when a document you created is edited": "Receive a notification when a document you created is edited",
|
||||||
"Collection created": "Collection created",
|
"Collection created": "Collection created",
|
||||||
"Receive a notification whenever a new collection is created": "Receive a notification whenever a new collection is created",
|
"Receive a notification whenever a new collection is created": "Receive a notification whenever a new collection is created",
|
||||||
@@ -597,9 +602,7 @@
|
|||||||
"Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?": "Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?",
|
"Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?": "Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?",
|
||||||
"Something went wrong while authenticating your request. Please try logging in again?": "Something went wrong while authenticating your request. Please try logging in again?",
|
"Something went wrong while authenticating your request. Please try logging in again?": "Something went wrong while authenticating your request. Please try logging in again?",
|
||||||
"Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.": "Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.",
|
"Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.": "Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.",
|
||||||
"Disconnect": "Disconnect",
|
|
||||||
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
|
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
|
||||||
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
|
|
||||||
"Connect": "Connect",
|
"Connect": "Connect",
|
||||||
"The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.": "The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.",
|
"The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.": "The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.",
|
||||||
"New token": "New token",
|
"New token": "New token",
|
||||||
|
|||||||
Reference in New Issue
Block a user