Skip to content

Commit da467ba

Browse files
authored
feat(site): add custom notification settings (#19938)
## Description Add Custom Notification settings to `/deployment/notifications` page and `/settings/notifications` user's page. <img width="2936" height="3008" alt="Screenshot 2025-09-24 at 12 53 52" src="https://github.com/user-attachments/assets/c11ccaba-7bdd-4b0d-98b3-faa48a9796ba" /> <img width="2920" height="2976" alt="Screenshot 2025-09-24 at 12 54 06" src="https://github.com/user-attachments/assets/21aa3057-a14e-4ba6-8138-98a4dc3952dc" /> Follow-up from: #19751
1 parent b712e97 commit da467ba

File tree

9 files changed

+273
-160
lines changed

9 files changed

+273
-160
lines changed

site/src/api/api.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2519,6 +2519,13 @@ class ApiMethods {
25192519
return res.data;
25202520
};
25212521

2522+
getCustomNotificationTemplates = async () => {
2523+
const res = await this.axios.get<TypesGen.NotificationTemplate[]>(
2524+
"/api/v2/notifications/templates/custom",
2525+
);
2526+
return res.data;
2527+
};
2528+
25222529
getNotificationDispatchMethods = async () => {
25232530
const res = await this.axios.get<TypesGen.NotificationMethodsResponse>(
25242531
"/api/v2/notifications/dispatch-methods",

site/src/api/queries/notifications.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ export const systemNotificationTemplates = () => {
6262
};
6363
};
6464

65+
export const customNotificationTemplatesKey = [
66+
"notifications",
67+
"templates",
68+
"custom",
69+
];
70+
71+
export const customNotificationTemplates = () => {
72+
return {
73+
queryKey: customNotificationTemplatesKey,
74+
queryFn: () => API.getCustomNotificationTemplates(),
75+
};
76+
};
77+
6578
export function selectTemplatesByGroup(
6679
data: NotificationTemplate[],
6780
): Record<string, NotificationTemplate[]> {
@@ -106,23 +119,24 @@ export const updateNotificationTemplateMethod = (
106119
mutationFn: (req: UpdateNotificationTemplateMethod) =>
107120
API.updateNotificationTemplateMethod(templateId, req),
108121
onMutate: (data) => {
109-
const prevData = queryClient.getQueryData<NotificationTemplate[]>(
122+
const keys = [
110123
systemNotificationTemplatesKey,
111-
);
112-
if (!prevData) {
113-
return;
124+
customNotificationTemplatesKey,
125+
];
126+
127+
for (const key of keys) {
128+
const prev = queryClient.getQueryData<NotificationTemplate[]>(key);
129+
if (!prev) {
130+
continue;
131+
}
132+
133+
queryClient.setQueryData(
134+
key,
135+
prev.map((tpl) =>
136+
tpl.id === templateId ? { ...tpl, method: data.method } : tpl,
137+
),
138+
);
114139
}
115-
queryClient.setQueryData(
116-
systemNotificationTemplatesKey,
117-
prevData.map((tpl) =>
118-
tpl.id === templateId
119-
? {
120-
...tpl,
121-
method: data.method,
122-
}
123-
: tpl,
124-
),
125-
);
126140
},
127141
} satisfies UseMutationOptions<
128142
void,

site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.stories.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MockNotificationTemplates } from "testHelpers/entities";
1+
import { MockSystemNotificationTemplates } from "testHelpers/entities";
22
import type { Meta, StoryObj } from "@storybook/react-vite";
33
import { API } from "api/api";
44
import { selectTemplatesByGroup } from "api/queries/notifications";
@@ -13,7 +13,7 @@ const meta: Meta<typeof NotificationEvents> = {
1313
args: {
1414
defaultMethod: "smtp",
1515
availableMethods: ["smtp", "webhook"],
16-
templatesByGroup: selectTemplatesByGroup(MockNotificationTemplates),
16+
templatesByGroup: selectTemplatesByGroup(MockSystemNotificationTemplates),
1717
deploymentConfig: baseMeta.parameters.deploymentValues,
1818
},
1919
...baseMeta,
@@ -60,7 +60,7 @@ export const Toggle: Story = {
6060
spyOn(API, "updateNotificationTemplateMethod").mockResolvedValue();
6161
const user = userEvent.setup();
6262
const canvas = within(canvasElement);
63-
const tmpl = MockNotificationTemplates[4];
63+
const tmpl = MockSystemNotificationTemplates[4];
6464
const option = await canvas.findByText(tmpl.name);
6565
const li = option.closest("li");
6666
if (!li) {
@@ -79,7 +79,7 @@ export const ToggleError: Story = {
7979
spyOn(API, "updateNotificationTemplateMethod").mockRejectedValue({});
8080
const user = userEvent.setup();
8181
const canvas = within(canvasElement);
82-
const tmpl = MockNotificationTemplates[4];
82+
const tmpl = MockSystemNotificationTemplates[4];
8383
const option = await canvas.findByText(tmpl.name);
8484
const li = option.closest("li");
8585
if (!li) {

site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.stories.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {
2+
MockCustomNotificationTemplates,
23
MockNotificationMethodsResponse,
3-
MockNotificationTemplates,
4+
MockSystemNotificationTemplates,
45
} from "testHelpers/entities";
56
import type { Meta, StoryObj } from "@storybook/react-vite";
67
import {
8+
customNotificationTemplatesKey,
79
notificationDispatchMethodsKey,
810
systemNotificationTemplatesKey,
911
} from "api/queries/notifications";
@@ -28,6 +30,10 @@ export const LoadingTemplates: Story = {
2830
key: systemNotificationTemplatesKey,
2931
data: undefined,
3032
},
33+
{
34+
key: customNotificationTemplatesKey,
35+
data: undefined,
36+
},
3137
{
3238
key: notificationDispatchMethodsKey,
3339
data: MockNotificationMethodsResponse,
@@ -39,7 +45,14 @@ export const LoadingTemplates: Story = {
3945
export const LoadingDispatchMethods: Story = {
4046
parameters: {
4147
queries: [
42-
{ key: systemNotificationTemplatesKey, data: MockNotificationTemplates },
48+
{
49+
key: systemNotificationTemplatesKey,
50+
data: MockSystemNotificationTemplates,
51+
},
52+
{
53+
key: customNotificationTemplatesKey,
54+
data: MockCustomNotificationTemplates,
55+
},
4356
{
4457
key: notificationDispatchMethodsKey,
4558
data: undefined,
@@ -48,7 +61,20 @@ export const LoadingDispatchMethods: Story = {
4861
},
4962
};
5063

51-
export const Events: Story = {};
64+
export const Events: Story = {
65+
play: async ({ canvasElement }) => {
66+
const canvas = within(canvasElement);
67+
68+
// System notification templates
69+
await canvas.findByText("Template Events");
70+
await canvas.findByText("User Events");
71+
await canvas.findByText("Workspace Events");
72+
73+
// Custom notification template
74+
await canvas.findByText("Custom Events");
75+
await canvas.findByText("Custom Notification");
76+
},
77+
};
5278

5379
export const Settings: Story = {
5480
play: async ({ canvasElement }) => {

site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationsPage.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Interpolation, Theme } from "@emotion/react";
22
import {
3+
customNotificationTemplates,
34
notificationDispatchMethods,
45
selectTemplatesByGroup,
56
systemNotificationTemplates,
@@ -27,21 +28,35 @@ import { Troubleshooting } from "./Troubleshooting";
2728

2829
const NotificationsPage: FC = () => {
2930
const { deploymentConfig } = useDeploymentConfig();
30-
const [templatesByGroup, dispatchMethods] = useQueries({
31-
queries: [
32-
{
33-
...systemNotificationTemplates(),
34-
select: selectTemplatesByGroup,
35-
},
36-
notificationDispatchMethods(),
37-
],
38-
});
31+
const [systemTemplatesByGroup, customTemplatesByGroup, dispatchMethods] =
32+
useQueries({
33+
queries: [
34+
{
35+
...systemNotificationTemplates(),
36+
select: selectTemplatesByGroup,
37+
},
38+
{
39+
...customNotificationTemplates(),
40+
select: selectTemplatesByGroup,
41+
},
42+
notificationDispatchMethods(),
43+
],
44+
});
3945
const tabState = useSearchParamsKey({
4046
key: "tab",
4147
defaultValue: "events",
4248
});
4349

44-
const ready = !!(templatesByGroup.data && dispatchMethods.data);
50+
const ready = !!(
51+
systemTemplatesByGroup.data &&
52+
customTemplatesByGroup.data &&
53+
dispatchMethods.data
54+
);
55+
// Combine system and custom notification templates
56+
const allTemplatesByGroup = {
57+
...systemTemplatesByGroup.data,
58+
...customTemplatesByGroup.data,
59+
};
4560
return (
4661
<>
4762
<Helmet>
@@ -79,7 +94,7 @@ const NotificationsPage: FC = () => {
7994
{ready ? (
8095
tabState.value === "events" ? (
8196
<NotificationEvents
82-
templatesByGroup={templatesByGroup.data}
97+
templatesByGroup={allTemplatesByGroup}
8398
deploymentConfig={deploymentConfig.config}
8499
defaultMethod={castNotificationMethod(
85100
dispatchMethods.data.default,

site/src/pages/DeploymentSettingsPage/NotificationsPage/storybookUtils.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
2+
MockCustomNotificationTemplates,
23
MockNotificationMethodsResponse,
3-
MockNotificationTemplates,
4+
MockSystemNotificationTemplates,
45
MockUserOwner,
56
} from "testHelpers/entities";
67
import {
@@ -11,6 +12,7 @@ import {
1112
} from "testHelpers/storybook";
1213
import type { Meta } from "@storybook/react-vite";
1314
import {
15+
customNotificationTemplatesKey,
1416
notificationDispatchMethodsKey,
1517
systemNotificationTemplatesKey,
1618
} from "api/queries/notifications";
@@ -187,7 +189,14 @@ export const baseMeta = {
187189
parameters: {
188190
experiments: ["notifications"],
189191
queries: [
190-
{ key: systemNotificationTemplatesKey, data: MockNotificationTemplates },
192+
{
193+
key: systemNotificationTemplatesKey,
194+
data: MockSystemNotificationTemplates,
195+
},
196+
{
197+
key: customNotificationTemplatesKey,
198+
data: MockCustomNotificationTemplates,
199+
},
191200
{
192201
key: notificationDispatchMethodsKey,
193202
data: MockNotificationMethodsResponse,

site/src/pages/UserSettingsPage/NotificationsPage/NotificationsPage.stories.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {
2+
MockCustomNotificationTemplates,
23
MockNotificationMethodsResponse,
34
MockNotificationPreferences,
4-
MockNotificationTemplates,
5+
MockSystemNotificationTemplates,
56
MockUserOwner,
67
} from "testHelpers/entities";
78
import {
@@ -12,6 +13,7 @@ import {
1213
import type { Meta, StoryObj } from "@storybook/react-vite";
1314
import { API } from "api/api";
1415
import {
16+
customNotificationTemplatesKey,
1517
notificationDispatchMethodsKey,
1618
systemNotificationTemplatesKey,
1719
userNotificationPreferencesKey,
@@ -32,7 +34,11 @@ const meta = {
3234
},
3335
{
3436
key: systemNotificationTemplatesKey,
35-
data: MockNotificationTemplates,
37+
data: MockSystemNotificationTemplates,
38+
},
39+
{
40+
key: customNotificationTemplatesKey,
41+
data: MockCustomNotificationTemplates,
3642
},
3743
{
3844
key: notificationDispatchMethodsKey,
@@ -100,7 +106,7 @@ if (!enabledPreference) {
100106
"No enabled notification preference available to test the disabling action.",
101107
);
102108
}
103-
const templateToDisable = MockNotificationTemplates.find(
109+
const templateToDisable = MockSystemNotificationTemplates.find(
104110
(tpl) => tpl.id === enabledPreference.id,
105111
);
106112
if (!templateToDisable) {

site/src/pages/UserSettingsPage/NotificationsPage/NotificationsPage.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import ListItemText, { listItemTextClasses } from "@mui/material/ListItemText";
88
import Switch from "@mui/material/Switch";
99
import Tooltip from "@mui/material/Tooltip";
1010
import {
11+
customNotificationTemplates,
1112
disableNotification,
1213
notificationDispatchMethods,
1314
selectTemplatesByGroup,
@@ -38,7 +39,12 @@ import { Section } from "../Section";
3839

3940
const NotificationsPage: FC = () => {
4041
const { user, permissions } = useAuthenticated();
41-
const [disabledPreferences, templatesByGroup, dispatchMethods] = useQueries({
42+
const [
43+
disabledPreferences,
44+
systemTemplatesByGroup,
45+
customTemplatesByGroup,
46+
dispatchMethods,
47+
] = useQueries({
4248
queries: [
4349
{
4450
...userNotificationPreferences(user.id),
@@ -48,6 +54,10 @@ const NotificationsPage: FC = () => {
4854
...systemNotificationTemplates(),
4955
select: (data: NotificationTemplate[]) => selectTemplatesByGroup(data),
5056
},
57+
{
58+
...customNotificationTemplates(),
59+
select: (data: NotificationTemplate[]) => selectTemplatesByGroup(data),
60+
},
5161
notificationDispatchMethods(),
5262
],
5363
});
@@ -80,7 +90,15 @@ const NotificationsPage: FC = () => {
8090
}, [searchParams.delete, disabledId, disableMutation]);
8191

8292
const ready =
83-
disabledPreferences.data && templatesByGroup.data && dispatchMethods.data;
93+
disabledPreferences.data &&
94+
systemTemplatesByGroup.data &&
95+
customTemplatesByGroup.data &&
96+
dispatchMethods.data;
97+
// Combine system and custom notification templates
98+
const allTemplatesByGroup = {
99+
...systemTemplatesByGroup.data,
100+
...customTemplatesByGroup.data,
101+
};
84102

85103
return (
86104
<>
@@ -94,7 +112,7 @@ const NotificationsPage: FC = () => {
94112
>
95113
{ready ? (
96114
<Stack spacing={4}>
97-
{Object.entries(templatesByGroup.data).map(([group, templates]) => {
115+
{Object.entries(allTemplatesByGroup).map(([group, templates]) => {
98116
if (!canSeeNotificationGroup(group, permissions)) {
99117
return null;
100118
}
@@ -218,6 +236,8 @@ function canSeeNotificationGroup(
218236
return permissions.createTemplates;
219237
case "User Events":
220238
return permissions.createUser;
239+
case "Custom Events":
240+
return true;
221241
default:
222242
return false;
223243
}

0 commit comments

Comments
 (0)