Skip to content

Commit 42efb2a

Browse files
authored
Improve slack notifications (#965)
* Improve slack notifications * types * types * restore page links * refactor: rename T-prefixed types to Slack-prefixed for better naming convention As suggested in PR review, using 'Slack' prefix instead of 'T' for type names provides better clarity and follows TypeScript naming conventions
1 parent 45aa646 commit 42efb2a

File tree

4 files changed

+187
-84
lines changed

4 files changed

+187
-84
lines changed

.changeset/empty-eggs-matter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/integration-slack': minor
3+
---
4+
5+
Improved slack notifications

bun.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
},
1717
"integrations/ahrefs": {
1818
"name": "@gitbook/integration-ahrefs",
19-
"version": "0.1.0",
19+
"version": "0.1.1",
2020
"dependencies": {
2121
"@gitbook/api": "*",
2222
"@gitbook/runtime": "*",
@@ -383,7 +383,7 @@
383383
},
384384
"integrations/mermaid": {
385385
"name": "@gitbook/integration-mermaid",
386-
"version": "0.4.0",
386+
"version": "0.4.2",
387387
"dependencies": {
388388
"@gitbook/api": "*",
389389
"@gitbook/runtime": "*",
@@ -433,7 +433,7 @@
433433
},
434434
"integrations/plausible": {
435435
"name": "@gitbook/integration-plausible",
436-
"version": "0.7.0",
436+
"version": "0.9.0",
437437
"dependencies": {
438438
"@gitbook/api": "*",
439439
"@gitbook/runtime": "*",
@@ -507,7 +507,7 @@
507507
},
508508
"integrations/segment": {
509509
"name": "@gitbook/integration-segment",
510-
"version": "2.1.3",
510+
"version": "2.3.0",
511511
"dependencies": {
512512
"@gitbook/api": "*",
513513
"@gitbook/runtime": "*",
@@ -652,7 +652,7 @@
652652
},
653653
"packages/api": {
654654
"name": "@gitbook/api",
655-
"version": "0.135.0",
655+
"version": "0.137.0",
656656
"dependencies": {
657657
"event-iterator": "^2.0.0",
658658
"eventsource-parser": "^3.0.0",

integrations/slack/src/index.ts

Lines changed: 154 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createIntegration, EventCallback } from '@gitbook/runtime';
44
import { SlackRuntimeContext } from './configuration';
55
import { handleFetchEvent } from './router';
66
import { slackAPI } from './slack';
7-
import { Spacer } from './ui';
7+
import { SlackBlock, SlackButtonElement } from './types';
88

99
/*
1010
* Handle content being updated: send a notification on Slack.
@@ -41,29 +41,6 @@ const handleSpaceContentUpdated: EventCallback<
4141
return;
4242
}
4343

44-
/*
45-
* Build a notification that looks something like this:
46-
*
47-
* Content of *Space* has been updated.
48-
*
49-
* [Changes were merged from change request: #123 - My Change Request]
50-
*
51-
* *New pages:*
52-
* • Page 7
53-
* • Page 8
54-
* • Page 9
55-
*
56-
* *Modified pages:*
57-
* • Page 1
58-
* • Page 2
59-
*
60-
* *New files:*
61-
* • File 1
62-
* • File 2
63-
*
64-
* And another X changes not listed here.
65-
*/
66-
6744
const createdPages: ChangedRevisionPage[] = [];
6845
const editedPages: ChangedRevisionPage[] = [];
6946
const deletedPages: ChangedRevisionPage[] = [];
@@ -100,75 +77,173 @@ const handleSpaceContentUpdated: EventCallback<
10077
}
10178
});
10279

103-
let notificationText = `Content of *<${space.urls.app}|${
104-
space.title || 'Space'
105-
}>* has been updated.\n\n`;
80+
const MAX_ITEMS_TO_SHOW = 5;
10681

107-
if (semanticChanges.mergedFrom) {
108-
const changeRequest = semanticChanges.mergedFrom;
109-
notificationText += `_Changes were merged from change request: <${changeRequest.urls.app}|#${changeRequest.number} - ${changeRequest.subject}>_\n\n`;
110-
}
82+
const createChangeSection = (
83+
title: string,
84+
items: string[] | ChangedRevisionPage[],
85+
emoji: string,
86+
): SlackBlock[] => {
87+
if (items.length === 0) return [];
11188

112-
const renderList = (list: string[]) => {
113-
return list.map((item) => `• ${item}\n`).join('');
114-
};
89+
const displayItems = items.slice(0, MAX_ITEMS_TO_SHOW);
90+
const remainingCount = items.length - displayItems.length;
11591

116-
const renderPageList = (list: ChangedRevisionPage[]) => {
117-
return list.map((item) => `• <${space.urls.app}${item.path}|${item.title}>\n`).join('');
118-
};
92+
let text = `${emoji} *${title}*\n`;
93+
displayItems.forEach((item) => {
94+
if (typeof item === 'string') {
95+
text += `• ${item}\n`;
96+
} else {
97+
// Page object with URL
98+
const pageUrl = `${space.urls.published || space.urls.app}${item.path || ''}`;
99+
text += `• <${pageUrl}|${item.title}>\n`;
100+
}
101+
});
119102

120-
if (
121-
createdPages.length > 0 ||
122-
editedPages.length > 0 ||
123-
deletedPages.length > 0 ||
124-
movedPages.length > 0 ||
125-
createdFiles.length > 0 ||
126-
editedFiles.length > 0 ||
127-
deletedFiles.length > 0
128-
) {
129-
if (createdPages.length > 0) {
130-
notificationText += `\n*New pages:*\n${renderPageList(createdPages)}\n\n`;
131-
}
132-
if (editedPages.length > 0) {
133-
notificationText += `\n*Modified pages:*\n${renderPageList(editedPages)}\n\n`;
134-
}
135-
if (deletedPages.length > 0) {
136-
notificationText += `\n*Deleted pages:*\n${renderPageList(deletedPages)}\n\n`;
137-
}
138-
if (movedPages.length > 0) {
139-
notificationText += `\n*Moved pages:*\n${renderPageList(movedPages)}\n\n`;
140-
}
141-
if (createdFiles.length > 0) {
142-
notificationText += `\n*New files:*\n${renderList(createdFiles)}\n\n`;
143-
}
144-
if (editedFiles.length > 0) {
145-
notificationText += `\n*Modified files:*\n${renderList(editedFiles)}\n\n`;
146-
}
147-
if (deletedFiles.length > 0) {
148-
notificationText += `\n*Deleted files:*\n${renderList(deletedFiles)}\n\n`;
103+
if (remainingCount > 0) {
104+
text += `_and ${remainingCount} more..._`;
149105
}
150106

151-
if (semanticChanges.more && semanticChanges.more > 0) {
152-
notificationText += `\n\nAnd another ${semanticChanges.more} changes not listed here.\n`;
107+
return [
108+
{
109+
type: 'section',
110+
text: {
111+
type: 'mrkdwn',
112+
text: text.trim(),
113+
},
114+
},
115+
];
116+
};
117+
118+
const getEmojiFromUnicode = (unicodeHex: string) => {
119+
if (!unicodeHex) return '✨';
120+
try {
121+
// Convert hex string to actual emoji
122+
const codePoint = parseInt(unicodeHex, 16);
123+
return String.fromCodePoint(codePoint);
124+
} catch {
125+
return '✨';
153126
}
127+
};
128+
129+
const spaceEmoji = space.emoji ? getEmojiFromUnicode(space.emoji) : '✨';
130+
131+
const blocks: SlackBlock[] = [
132+
{
133+
type: 'header',
134+
text: {
135+
type: 'plain_text',
136+
text: `${spaceEmoji} ${space.title || 'Space'} Updated`,
137+
emoji: true,
138+
},
139+
},
140+
];
141+
142+
let changeRequest;
143+
if (semanticChanges.mergedFrom) {
144+
changeRequest = semanticChanges.mergedFrom;
145+
blocks.push({
146+
type: 'context',
147+
elements: [
148+
{
149+
type: 'mrkdwn',
150+
text: `🔀 Changes were merged from change request: <${changeRequest.urls.app}|#${changeRequest.number}${changeRequest.subject ? ` - ${changeRequest.subject}` : ''}>`,
151+
},
152+
],
153+
});
154154
}
155155

156+
blocks.push({
157+
type: 'section',
158+
text: {
159+
type: 'mrkdwn',
160+
text: '*Summary of Changes:*',
161+
},
162+
});
163+
164+
blocks.push(...createChangeSection('New pages', createdPages, '🆕'));
165+
blocks.push(...createChangeSection('Modified pages', editedPages, '📝'));
166+
blocks.push(...createChangeSection('Deleted pages', deletedPages, '🗑️'));
167+
blocks.push(...createChangeSection('Moved pages', movedPages, '📁'));
168+
blocks.push(...createChangeSection('New files', createdFiles, '📄'));
169+
blocks.push(...createChangeSection('Modified files', editedFiles, '📝'));
170+
blocks.push(...createChangeSection('Deleted files', deletedFiles, '🗂️'));
171+
172+
if (semanticChanges.more && semanticChanges.more > 0) {
173+
blocks.push({
174+
type: 'context',
175+
elements: [
176+
{
177+
type: 'mrkdwn',
178+
text: `➕ _And ${semanticChanges.more} additional changes not listed here_`,
179+
},
180+
],
181+
});
182+
}
183+
184+
const actionButtons: SlackButtonElement[] = [
185+
{
186+
type: 'button',
187+
text: {
188+
type: 'plain_text',
189+
text: '🏠 View Main Space',
190+
emoji: true,
191+
},
192+
url: space.urls.app,
193+
action_id: 'view_main_space',
194+
style: 'primary',
195+
},
196+
];
197+
198+
if (space.urls.published) {
199+
actionButtons.push({
200+
type: 'button',
201+
text: {
202+
type: 'plain_text',
203+
text: '🌐 View Docs Site',
204+
emoji: true,
205+
},
206+
url: space.urls.published,
207+
action_id: 'view_docs_site',
208+
});
209+
}
210+
211+
blocks.push({ type: 'divider' });
212+
213+
// Add a nice footer section
214+
const totalChanges =
215+
createdPages.length +
216+
editedPages.length +
217+
deletedPages.length +
218+
movedPages.length +
219+
createdFiles.length +
220+
editedFiles.length +
221+
deletedFiles.length +
222+
(semanticChanges.more || 0);
223+
224+
blocks.push({
225+
type: 'context',
226+
elements: [
227+
{
228+
type: 'mrkdwn',
229+
text: `📊 *${totalChanges} total changes* • Updated just now`,
230+
},
231+
],
232+
});
233+
234+
blocks.push({
235+
type: 'actions',
236+
elements: actionButtons,
237+
});
238+
156239
await slackAPI(context, {
157240
method: 'POST',
158241
path: 'chat.postMessage',
159242
payload: {
160243
channel,
161-
blocks: [
162-
Spacer,
163-
{
164-
type: 'section',
165-
text: {
166-
type: 'mrkdwn',
167-
text: notificationText,
168-
},
169-
},
170-
Spacer,
171-
],
244+
blocks,
245+
unfurl_links: false,
246+
unfurl_media: false,
172247
},
173248
});
174249
};

integrations/slack/src/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Slack Block Kit Types
2+
export type SlackTextType = 'plain_text' | 'mrkdwn';
3+
4+
export type SlackTextField = {
5+
type: SlackTextType;
6+
text: string;
7+
emoji?: boolean;
8+
};
9+
10+
export type SlackButtonElement = {
11+
type: 'button';
12+
text: SlackTextField;
13+
url?: string;
14+
action_id: string;
15+
style?: 'primary' | 'danger';
16+
};
17+
18+
export type SlackBlock =
19+
| { type: 'section'; text?: SlackTextField; fields?: SlackTextField[]; accessory?: unknown }
20+
| { type: 'header'; text: SlackTextField }
21+
| { type: 'context'; elements: SlackTextField[] }
22+
| { type: 'divider' }
23+
| { type: 'actions'; elements: SlackButtonElement[] };

0 commit comments

Comments
 (0)