feat: turn off notification via email (#14520)

This commit is contained in:
Joobi S B
2024-09-11 11:10:24 -03:00
committed by GitHub
parent 5bd19f8ba3
commit 3301212972
9 changed files with 153 additions and 14 deletions
+10 -7
View File
@@ -3499,6 +3499,7 @@ func (q *sqlQuerier) EnqueueNotificationMessage(ctx context.Context, arg Enqueue
const fetchNewMessageMetadata = `-- name: FetchNewMessageMetadata :one
SELECT nt.name AS notification_name,
nt.id AS notification_template_id,
nt.actions AS actions,
nt.method AS custom_method,
u.id AS user_id,
@@ -3517,13 +3518,14 @@ type FetchNewMessageMetadataParams struct {
}
type FetchNewMessageMetadataRow struct {
NotificationName string `db:"notification_name" json:"notification_name"`
Actions []byte `db:"actions" json:"actions"`
CustomMethod NullNotificationMethod `db:"custom_method" json:"custom_method"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
UserEmail string `db:"user_email" json:"user_email"`
UserName string `db:"user_name" json:"user_name"`
UserUsername string `db:"user_username" json:"user_username"`
NotificationName string `db:"notification_name" json:"notification_name"`
NotificationTemplateID uuid.UUID `db:"notification_template_id" json:"notification_template_id"`
Actions []byte `db:"actions" json:"actions"`
CustomMethod NullNotificationMethod `db:"custom_method" json:"custom_method"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
UserEmail string `db:"user_email" json:"user_email"`
UserName string `db:"user_name" json:"user_name"`
UserUsername string `db:"user_username" json:"user_username"`
}
// This is used to build up the notification_message's JSON payload.
@@ -3532,6 +3534,7 @@ func (q *sqlQuerier) FetchNewMessageMetadata(ctx context.Context, arg FetchNewMe
var i FetchNewMessageMetadataRow
err := row.Scan(
&i.NotificationName,
&i.NotificationTemplateID,
&i.Actions,
&i.CustomMethod,
&i.UserID,
@@ -1,6 +1,7 @@
-- name: FetchNewMessageMetadata :one
-- This is used to build up the notification_message's JSON payload.
SELECT nt.name AS notification_name,
nt.id AS notification_template_id,
nt.actions AS actions,
nt.method AS custom_method,
u.id AS user_id,
@@ -26,6 +26,7 @@
<div style="border-top: 1px solid #e2e8f0; color: #475569; font-size: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
<p>&copy;&nbsp;{{ current_year }}&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a href="{{ base_url }}" style="color: #2563eb; text-decoration: none;">{{ base_url }}</a></p>
<p><a href="{{ base_url }}/settings/notifications" style="color: #2563eb; text-decoration: none;">Click here to manage your notification settings</a></p>
<p><a href="{{ base_url }}/settings/notifications?disabled={{ .NotificationTemplateID }}" style="color: #2563eb; text-decoration: none;">Stop receiving emails like this</a></p>
</div>
</div>
</body>
+3 -2
View File
@@ -121,9 +121,10 @@ func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUI
// actions which can be taken by the recipient.
func (s *StoreEnqueuer) buildPayload(metadata database.FetchNewMessageMetadataRow, labels map[string]string) (*types.MessagePayload, error) {
payload := types.MessagePayload{
Version: "1.0",
Version: "1.1",
NotificationName: metadata.NotificationName,
NotificationName: metadata.NotificationName,
NotificationTemplateID: metadata.NotificationTemplateID.String(),
UserID: metadata.UserID.String(),
UserEmail: metadata.UserEmail,
@@ -56,6 +56,15 @@ func TestGoTemplate(t *testing.T) {
"url": "https://mocked-server-address/@johndoe/my-workspace"
}]`,
},
{
name: "render notification template ID",
in: `{{ .NotificationTemplateID }}`,
payload: types.MessagePayload{
NotificationTemplateID: "4e19c0ac-94e1-4532-9515-d1801aa283b2",
},
expectedOutput: "4e19c0ac-94e1-4532-9515-d1801aa283b2",
expectedErr: nil,
},
}
for _, tc := range tests {
+2 -1
View File
@@ -7,7 +7,8 @@ package types
type MessagePayload struct {
Version string `json:"_version"`
NotificationName string `json:"notification_name"`
NotificationName string `json:"notification_name"`
NotificationTemplateID string `json:"notification_template_id"`
UserID string `json:"user_id"`
UserEmail string `json:"user_email"`
+19
View File
@@ -136,3 +136,22 @@ export const updateNotificationTemplateMethod = (
UpdateNotificationTemplateMethod
>;
};
export const disableNotification = (
userId: string,
queryClient: QueryClient,
) => {
return {
mutationFn: async (templateId: string) => {
const result = await API.putUserNotificationPreferences(userId, {
template_disabled_map: {
[templateId]: true,
},
});
return result;
},
onSuccess: (data) => {
queryClient.setQueryData(userNotificationPreferencesKey(userId), data);
},
} satisfies UseMutationOptions<NotificationPreference[], unknown, string>;
};
@@ -1,11 +1,13 @@
import type { Meta, StoryObj } from "@storybook/react";
import { spyOn, userEvent, within } from "@storybook/test";
import { expect, spyOn, userEvent, waitFor, within } from "@storybook/test";
import { API } from "api/api";
import {
notificationDispatchMethodsKey,
systemNotificationTemplatesKey,
userNotificationPreferencesKey,
} from "api/queries/notifications";
import { http, HttpResponse } from "msw";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import {
MockNotificationMethodsResponse,
MockNotificationPreferences,
@@ -19,7 +21,7 @@ import {
} from "testHelpers/storybook";
import { NotificationsPage } from "./NotificationsPage";
const meta: Meta<typeof NotificationsPage> = {
const meta = {
title: "pages/UserSettingsPage/NotificationsPage",
component: NotificationsPage,
parameters: {
@@ -42,7 +44,7 @@ const meta: Meta<typeof NotificationsPage> = {
permissions: { viewDeploymentValues: true },
},
decorators: [withGlobalSnackbar, withAuthProvider, withDashboardProvider],
};
} satisfies Meta<typeof NotificationsPage>;
export default meta;
type Story = StoryObj<typeof NotificationsPage>;
@@ -76,3 +78,78 @@ export const NonAdmin: Story = {
permissions: { viewDeploymentValues: false },
},
};
// Ensure the selected notification template is enabled before attempting to
// disable it.
const enabledPreference = MockNotificationPreferences.find(
(pref) => pref.disabled === false,
);
if (!enabledPreference) {
throw new Error(
"No enabled notification preference available to test the disabling action.",
);
}
const templateToDisable = MockNotificationTemplates.find(
(tpl) => tpl.id === enabledPreference.id,
);
if (!templateToDisable) {
throw new Error(" No notification template matches the enabled preference.");
}
export const DisableValidTemplate: Story = {
parameters: {
reactRouter: reactRouterParameters({
location: {
searchParams: { disabled: templateToDisable.id },
},
}),
},
decorators: [
(Story) => {
// Since the action occurs during the initial render, we need to spy on
// the API call before the story is rendered. This is done using a
// decorator to ensure the spy is set up in time.
spyOn(API, "putUserNotificationPreferences").mockResolvedValue(
MockNotificationPreferences.map((pref) => {
if (pref.id === templateToDisable.id) {
return {
...pref,
disabled: true,
};
}
return pref;
}),
);
return <Story />;
},
],
play: async ({ canvasElement }) => {
await within(document.body).findByText("Notification has been disabled");
const switchEl = await within(canvasElement).findByLabelText(
templateToDisable.name,
);
expect(switchEl).not.toBeChecked();
},
};
export const DisableInvalidTemplate: Story = {
parameters: {
reactRouter: reactRouterParameters({
location: {
searchParams: { disabled: "invalid-template-id" },
},
}),
},
decorators: [
(Story) => {
// Since the action occurs during the initial render, we need to spy on
// the API call before the story is rendered. This is done using a
// decorator to ensure the spy is set up in time.
spyOn(API, "putUserNotificationPreferences").mockRejectedValue({});
return <Story />;
},
],
play: async () => {
await within(document.body).findByText("Error disabling notification");
},
};
@@ -8,6 +8,7 @@ import ListItemText, { listItemTextClasses } from "@mui/material/ListItemText";
import Switch from "@mui/material/Switch";
import Tooltip from "@mui/material/Tooltip";
import {
disableNotification,
notificationDispatchMethods,
selectTemplatesByGroup,
systemNotificationTemplates,
@@ -18,7 +19,7 @@ import type {
NotificationPreference,
NotificationTemplate,
} from "api/typesGenerated";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { Stack } from "components/Stack/Stack";
import { useAuthenticated } from "contexts/auth/RequireAuth";
@@ -28,8 +29,10 @@ import {
methodLabels,
} from "modules/notifications/utils";
import { type FC, Fragment } from "react";
import { useEffect } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQueries, useQueryClient } from "react-query";
import { useSearchParams } from "react-router-dom";
import { pageTitle } from "utils/page";
import { Section } from "../Section";
@@ -60,6 +63,30 @@ export const NotificationsPage: FC = () => {
const updatePreferences = useMutation(
updateUserNotificationPreferences(user.id, queryClient),
);
// Notification emails contain a link to disable a specific notification
// template. This functionality is achieved using the query string parameter
// "disabled".
const disableMutation = useMutation(
disableNotification(user.id, queryClient),
);
const [searchParams] = useSearchParams();
const disabledId = searchParams.get("disabled");
useEffect(() => {
if (!disabledId) {
return;
}
searchParams.delete("disabled");
disableMutation
.mutateAsync(disabledId)
.then(() => {
displaySuccess("Notification has been disabled");
})
.catch(() => {
displayError("Error disabling notification");
});
}, [searchParams.delete, disabledId, disableMutation]);
const ready =
disabledPreferences.data && templatesByGroup.data && dispatchMethods.data;