* Add test notification tool

* Add frontend styles

* Remove option from admin view

* Refactor create post and add translations

* Fix several CI errors

* Fix API and frontend snapshots

* Refactor trailing and leading icon on buttons

* Add different button states

* i18n-extract

* Fix wrong text

* Add tests

* Fix wrong string

* Fix test

* feat: E2E send test notifications (#28371)

* Refactor send desktop notification

* Address rest of the feedback

* Fix tests

* Add correct link

* Fix test

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
Этот коммит содержится в:
Daniel Espino García
2024-11-08 13:57:06 +01:00
коммит произвёл GitHub
родитель 30a6ddc995
Коммит 118d0346ee
31 изменённых файлов: 1170 добавлений и 56 удалений

Просмотреть файл

@@ -331,6 +331,34 @@ describe('notification_actions', () => {
});
});
test('should notify for forced notification posts on muted channels', () => {
const store = testConfigureStore(baseState);
const newPost = {
...post,
props: {
...post.props,
force_notification: 'test',
},
};
newPost.channel_id = 'muted_channel_id';
const newMsgProps = {
post: JSON.stringify(newPost),
channel_display_name: 'Muted Channel',
team_id: 'team_id',
};
return store.dispatch(sendDesktopNotification(newPost, newMsgProps)).then((result) => {
expect(result).toEqual({data: {status: 'success'}});
expect(spy).toHaveBeenCalledWith({
body: '@username: Where is Jessica Hyde?',
requireInteraction: false,
silent: false,
title: 'Muted Channel',
onClick: expect.any(Function),
});
});
});
test.each([
UserStatuses.DND,
UserStatuses.OUT_OF_OFFICE,

Просмотреть файл

@@ -8,6 +8,7 @@ import type {Post} from '@mattermost/types/posts';
import type {UserProfile} from '@mattermost/types/users';
import {logError} from 'mattermost-redux/actions/errors';
import {Client4} from 'mattermost-redux/client';
import {getCurrentChannel, getMyChannelMember, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {
@@ -117,6 +118,7 @@ export function sendDesktopNotification(post: Post, msgProps: NewPostMessageProp
const user = getCurrentUser(state);
const member = getMyChannelMember(state, post.channel_id);
const isCrtReply = isCollapsedThreadsEnabled(state) && post.root_id !== '';
const forceNotification = Boolean(post.props?.force_notification);
const skipNotificationReason = shouldSkipNotification(
state,
@@ -125,6 +127,7 @@ export function sendDesktopNotification(post: Post, msgProps: NewPostMessageProp
user,
channel,
member,
forceNotification,
isCrtReply,
);
if (skipNotificationReason) {
@@ -156,7 +159,7 @@ export function sendDesktopNotification(post: Post, msgProps: NewPostMessageProp
const argsAfterHooks = hookResult.data!;
if (!argsAfterHooks.notify) {
if (!argsAfterHooks.notify && !forceNotification) {
return {data: {status: 'not_sent', reason: 'desktop_notification_hook', data: String(hookResult)}};
}
@@ -254,6 +257,7 @@ function shouldSkipNotification(
user: UserProfile,
channel: Pick<Channel, 'type' | 'id'>,
member: ChannelMembership | undefined,
skipChecks: boolean,
isCrtReply: boolean,
) {
const currentUserId = getCurrentUserId(state);
@@ -269,6 +273,10 @@ function shouldSkipNotification(
return {status: 'error', reason: 'no_member'};
}
if (skipChecks) {
return undefined;
}
if (isChannelMuted(member)) {
return {status: 'not_sent', reason: 'channel_muted'};
}
@@ -428,3 +436,12 @@ export function notifyMe(title: string, body: string, channelId: string, teamId:
}
};
}
export const sendTestNotification = async () => {
try {
const result = await Client4.sendTestNotificaiton();
return result;
} catch (error) {
return error;
}
};

Просмотреть файл

@@ -0,0 +1,132 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {DeepPartial} from '@mattermost/types/utilities';
import {renderHookWithContext} from 'tests/react_testing_utils';
import type {GlobalState} from 'types/store';
import {useExternalLink} from './use_external_link';
const baseCurrentUserId = 'someUserId';
const baseTelemetryId = 'someTelemetryId';
function getBaseState(): DeepPartial<GlobalState> {
return {
entities: {
users: {
currentUserId: baseCurrentUserId,
},
general: {
config: {
TelemetryId: baseTelemetryId,
},
license: {
Cloud: 'true',
},
},
},
};
}
describe('useExternalLink', () => {
it('keep non mattermost links untouched', () => {
const url = 'https://www.someLink.com/something?query1=2#anchor';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(url, 'some location', {utm_source: 'something'}), getBaseState());
expect(href).toEqual(url);
expect(queryParams).toEqual({});
});
it('all base queries are set correctly', () => {
const url = 'https://www.mattermost.com/some/url';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(url), getBaseState());
const parsedLink = new URL(href);
expect(parsedLink.searchParams.get('utm_source')).toBe('mattermost');
expect(parsedLink.searchParams.get('utm_medium')).toBe('in-product-cloud');
expect(parsedLink.searchParams.get('utm_content')).toBe('');
expect(parsedLink.searchParams.get('uid')).toBe(baseCurrentUserId);
expect(parsedLink.searchParams.get('sid')).toBe(baseTelemetryId);
expect(queryParams.utm_source).toBe('mattermost');
expect(queryParams.utm_medium).toBe('in-product-cloud');
expect(queryParams.utm_content).toBe('');
expect(queryParams.uid).toBe(baseCurrentUserId);
expect(queryParams.sid).toBe(baseTelemetryId);
expect(href.split('?')[0]).toBe(url);
});
it('provided location is added to the params', () => {
const url = 'https://www.mattermost.com/some/url';
const location = 'someLocation';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(url, location), getBaseState());
const parsedLink = new URL(href);
expect(parsedLink.searchParams.get('utm_content')).toBe(location);
expect(queryParams.utm_content).toBe(location);
});
it('non cloud environments set the proper utm medium', () => {
const url = 'https://www.mattermost.com/some/url';
const state = getBaseState();
state.entities!.general!.license!.Cloud = 'false';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(url), state);
const parsedLink = new URL(href);
expect(parsedLink.searchParams.get('utm_medium')).toBe('in-product');
expect(queryParams.utm_medium).toBe('in-product');
});
it('keep existing query parameters untouched', () => {
const url = 'https://www.mattermost.com/some/url?myParameter=true';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(url), getBaseState());
const parsedLink = new URL(href);
expect(parsedLink.searchParams.get('myParameter')).toBe('true');
expect(queryParams.myParameter).toBe('true');
});
it('keep anchors untouched', () => {
const url = 'https://www.mattermost.com/some/url?myParameter=true#myAnchor';
const {result: {current: [href]}} = renderHookWithContext(() => useExternalLink(url), getBaseState());
const parsedLink = new URL(href);
expect(parsedLink.hash).toBe('#myAnchor');
});
it('overwriting params gets preference over default params', () => {
const url = 'https://www.mattermost.com/some/url';
const location = 'someLocation';
const expectedContent = 'someOtherLocation';
const expectedSource = 'someOtherSource';
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(url, location, {utm_content: expectedContent, utm_source: expectedSource}), getBaseState());
const parsedLink = new URL(href);
expect(parsedLink.searchParams.get('utm_content')).toBe(expectedContent);
expect(queryParams.utm_content).toBe(expectedContent);
expect(parsedLink.searchParams.get('utm_source')).toBe(expectedSource);
expect(queryParams.utm_source).toBe(expectedSource);
});
it('existing params gets preference over default and overwritten params', () => {
const location = 'someLocation';
const overwrittenContent = 'someOtherLocation';
const overwrittenSource = 'someOtherSource';
const expectedContent = 'differentLocation';
const expectedSource = 'differentSource';
const url = `https://www.mattermost.com/some/url?utm_content=${expectedContent}&utm_source=${expectedSource}`;
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(url, location, {utm_content: overwrittenContent, utm_source: overwrittenSource}), getBaseState());
const parsedLink = new URL(href);
expect(parsedLink.searchParams.get('utm_content')).toBe(expectedContent);
expect(queryParams.utm_content).toBe(expectedContent);
expect(parsedLink.searchParams.get('utm_source')).toBe(expectedSource);
expect(queryParams.utm_source).toBe(expectedSource);
});
it('results are stable between re-renders', () => {
const url = 'https://www.mattermost.com/some/url';
const overwriteQueryParams = {utm_content: 'overwrittenContent', utm_source: 'overwrittenSource'};
const {result, rerender} = renderHookWithContext(() => useExternalLink(url, 'someLocation', overwriteQueryParams), getBaseState());
const [firstHref, firstParams] = result.current;
rerender();
const [secondHref, secondParams] = result.current;
expect(firstHref).toBe(secondHref);
expect(firstParams).toBe(secondParams);
});
});

Просмотреть файл

@@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {useMemo} from 'react';
import {useSelector} from 'react-redux';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import type {GlobalState} from 'types/store';
export type ExternalLinkQueryParams = {
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
utm_content?: string;
userId?: string;
}
export function useExternalLink(href: string, location: string = '', overwriteQueryParams: ExternalLinkQueryParams = {}): [string, Record<string, string>] {
const userId = useSelector(getCurrentUserId);
const telemetryId = useSelector((state: GlobalState) => getConfig(state).TelemetryId || '');
const isCloud = useSelector((state: GlobalState) => getLicense(state).Cloud === 'true');
return useMemo(() => {
if (!href?.includes('mattermost.com')) {
return [href, {}];
}
const parsedUrl = new URL(href);
const existingURLSearchParams = parsedUrl.searchParams;
const existingQueryParamsObj = Object.fromEntries(existingURLSearchParams.entries());
const queryParams = {
utm_source: 'mattermost',
utm_medium: isCloud ? 'in-product-cloud' : 'in-product',
utm_content: location,
uid: userId,
sid: telemetryId,
...overwriteQueryParams,
...existingQueryParamsObj,
};
parsedUrl.search = new URLSearchParams(queryParams).toString();
return [parsedUrl.toString(), queryParams];
}, [href, isCloud, location, overwriteQueryParams, telemetryId, userId]);
}

Просмотреть файл

@@ -18,7 +18,7 @@ exports[`components/external_link should match snapshot 1`] = `
location="test"
>
<a
href="https://mattermost.com?utm_source=mattermost&utm_medium=in-product-cloud&utm_content=test&uid=currentUserId&sid="
href="https://mattermost.com/?utm_source=mattermost&utm_medium=in-product-cloud&utm_content=test&uid=currentUserId&sid="
location="test"
onClick={[Function]}
rel="noopener noreferrer"

Просмотреть файл

@@ -98,7 +98,7 @@ describe('components/external_link', () => {
expect(screen.queryByText('Click Me')).toHaveAttribute(
'href',
'https://mattermost.com?utm_source=mattermost&utm_medium=in-product-cloud&utm_content=test&uid=currentUserId&sid=&test=true',
'https://mattermost.com/?utm_source=mattermost&utm_medium=in-product-cloud&utm_content=test&uid=currentUserId&sid=&test=true',
);
});
@@ -191,7 +191,7 @@ describe('components/external_link', () => {
expect(screen.queryByText('Click Me')).toHaveAttribute(
'href',
'https://mattermost.com?utm_source=mattermost&utm_medium=in-product-cloud&utm_content=test&uid=currentUserId&sid=#desktop',
'https://mattermost.com/?utm_source=mattermost&utm_medium=in-product-cloud&utm_content=test&uid=currentUserId&sid=#desktop',
);
});
});

Просмотреть файл

@@ -4,20 +4,11 @@
/* eslint-disable @mattermost/use-external-link */
import React, {forwardRef} from 'react';
import {useSelector} from 'react-redux';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {trackEvent} from 'actions/telemetry_actions';
type ExternalLinkQueryParams = {
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
utm_content?: string;
userId?: string;
}
import type {ExternalLinkQueryParams} from 'components/common/hooks/use_external_link';
import {useExternalLink} from 'components/common/hooks/use_external_link';
type Props = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
href: string;
@@ -30,35 +21,7 @@ type Props = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
}
const ExternalLink = forwardRef<HTMLAnchorElement, Props>((props, ref) => {
const userId = useSelector(getCurrentUserId);
const config = useSelector(getConfig);
const license = useSelector(getLicense);
let href = props.href;
let queryParams = {};
if (href?.includes('mattermost.com')) {
const existingURLSearchParams = new URL(href).searchParams;
const existingQueryParamsObj = Object.fromEntries(existingURLSearchParams.entries());
queryParams = {
utm_source: 'mattermost',
utm_medium: license.Cloud === 'true' ? 'in-product-cloud' : 'in-product',
utm_content: props.location || '',
uid: userId,
sid: config.TelemetryId || '',
...props.queryParams,
...existingQueryParamsObj,
};
const queryString = new URLSearchParams(queryParams).toString();
if (Object.keys(existingQueryParamsObj).length) {
// If the href already has query params, remove them before adding them back with the addition of the new ones
href = href?.split('?')[0];
}
const anchor = new URL(href).hash;
if (anchor) {
href = href.replace(anchor, '');
}
href = `${href}?${queryString}${anchor ?? ''}`;
}
const [href, queryParams] = useExternalLink(props.href, props.location, props.queryParams);
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
trackEvent('link_out', 'click_external_link', queryParams);

Просмотреть файл

@@ -93,6 +93,7 @@ describe('components/MarketplaceItemPlugin', () => {
entities: {
general: {
config: {},
license: {},
},
users: {
currentUserId: 'currentUserId',

10
webapp/channels/src/components/section_notice/types.d.ts поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
type SectionNoticeButton = {
onClick: () => void;
text: string;
trailingIcon?: string;
leadingIcon?: string;
loading?: boolean;
}

Просмотреть файл

@@ -0,0 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/user_settings/notifications/send_test_notification_notice should match snapshot 1`] = `
<div>
<div
class="divider-light"
/>
<div
style="margin-top: 20px;"
>
<div
class="sectionNoticeContainer hint"
>
<div
class="sectionNoticeContent"
>
<i
class="icon sectionNoticeIcon icon-lightbulb-outline hint"
/>
<div
class="sectionNoticeBody"
>
<h4
class="sectionNoticeTitle"
>
Troubleshooting notifications
</h4>
<p>
Not receiving notifications? Start by sending a test notification to all your devices to check if theyre working as expected. If issues persist, explore ways to solve them with troubleshooting steps.
</p>
<div
class="sectionNoticeActions"
>
<button
class="btn btn-sm sectionNoticeButton btn-primary"
>
Send a test notification
</button>
<button
class="btn btn-sm sectionNoticeButton btn-tertiary"
>
Troubleshooting docs
<i
class="icon icon-open-in-new"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;

Просмотреть файл

@@ -258,8 +258,52 @@ Object {
class="divider-light"
/>
<div
class="divider-dark"
class="divider-light"
/>
<div
style="margin-top: 20px;"
>
<div
class="sectionNoticeContainer hint"
>
<div
class="sectionNoticeContent"
>
<i
class="icon sectionNoticeIcon icon-lightbulb-outline hint"
/>
<div
class="sectionNoticeBody"
>
<h4
class="sectionNoticeTitle"
>
Troubleshooting notifications
</h4>
<p>
Not receiving notifications? Start by sending a test notification to all your devices to check if theyre working as expected. If issues persist, explore ways to solve them with troubleshooting steps.
</p>
<div
class="sectionNoticeActions"
>
<button
class="btn btn-sm sectionNoticeButton btn-primary"
>
Send a test notification
</button>
<button
class="btn btn-sm sectionNoticeButton btn-tertiary"
>
Troubleshooting docs
<i
class="icon icon-open-in-new"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -518,8 +562,52 @@ Object {
class="divider-light"
/>
<div
class="divider-dark"
class="divider-light"
/>
<div
style="margin-top: 20px;"
>
<div
class="sectionNoticeContainer hint"
>
<div
class="sectionNoticeContent"
>
<i
class="icon sectionNoticeIcon icon-lightbulb-outline hint"
/>
<div
class="sectionNoticeBody"
>
<h4
class="sectionNoticeTitle"
>
Troubleshooting notifications
</h4>
<p>
Not receiving notifications? Start by sending a test notification to all your devices to check if theyre working as expected. If issues persist, explore ways to solve them with troubleshooting steps.
</p>
<div
class="sectionNoticeActions"
>
<button
class="btn btn-sm sectionNoticeButton btn-primary"
>
Send a test notification
</button>
<button
class="btn btn-sm sectionNoticeButton btn-tertiary"
>
Troubleshooting docs
<i
class="icon icon-open-in-new"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>,
@@ -840,8 +928,52 @@ Object {
</div>
</div>
<div
class="divider-dark"
class="divider-light"
/>
<div
style="margin-top: 20px;"
>
<div
class="sectionNoticeContainer hint"
>
<div
class="sectionNoticeContent"
>
<i
class="icon sectionNoticeIcon icon-lightbulb-outline hint"
/>
<div
class="sectionNoticeBody"
>
<h4
class="sectionNoticeTitle"
>
Troubleshooting notifications
</h4>
<p>
Not receiving notifications? Start by sending a test notification to all your devices to check if theyre working as expected. If issues persist, explore ways to solve them with troubleshooting steps.
</p>
<div
class="sectionNoticeActions"
>
<button
class="btn btn-sm sectionNoticeButton btn-primary"
>
Send a test notification
</button>
<button
class="btn btn-sm sectionNoticeButton btn-tertiary"
>
Troubleshooting docs
<i
class="icon icon-open-in-new"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -1103,8 +1235,52 @@ Object {
</div>
</div>
<div
class="divider-dark"
class="divider-light"
/>
<div
style="margin-top: 20px;"
>
<div
class="sectionNoticeContainer hint"
>
<div
class="sectionNoticeContent"
>
<i
class="icon sectionNoticeIcon icon-lightbulb-outline hint"
/>
<div
class="sectionNoticeBody"
>
<h4
class="sectionNoticeTitle"
>
Troubleshooting notifications
</h4>
<p>
Not receiving notifications? Start by sending a test notification to all your devices to check if theyre working as expected. If issues persist, explore ways to solve them with troubleshooting steps.
</p>
<div
class="sectionNoticeActions"
>
<button
class="btn btn-sm sectionNoticeButton btn-primary"
>
Send a test notification
</button>
<button
class="btn btn-sm sectionNoticeButton btn-tertiary"
>
Troubleshooting docs
<i
class="icon icon-open-in-new"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>,
@@ -1387,8 +1563,52 @@ Object {
class="divider-light"
/>
<div
class="divider-dark"
class="divider-light"
/>
<div
style="margin-top: 20px;"
>
<div
class="sectionNoticeContainer hint"
>
<div
class="sectionNoticeContent"
>
<i
class="icon sectionNoticeIcon icon-lightbulb-outline hint"
/>
<div
class="sectionNoticeBody"
>
<h4
class="sectionNoticeTitle"
>
Troubleshooting notifications
</h4>
<p>
Not receiving notifications? Start by sending a test notification to all your devices to check if theyre working as expected. If issues persist, explore ways to solve them with troubleshooting steps.
</p>
<div
class="sectionNoticeActions"
>
<button
class="btn btn-sm sectionNoticeButton btn-primary"
>
Send a test notification
</button>
<button
class="btn btn-sm sectionNoticeButton btn-tertiary"
>
Troubleshooting docs
<i
class="icon icon-open-in-new"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -1612,8 +1832,52 @@ Object {
class="divider-light"
/>
<div
class="divider-dark"
class="divider-light"
/>
<div
style="margin-top: 20px;"
>
<div
class="sectionNoticeContainer hint"
>
<div
class="sectionNoticeContent"
>
<i
class="icon sectionNoticeIcon icon-lightbulb-outline hint"
/>
<div
class="sectionNoticeBody"
>
<h4
class="sectionNoticeTitle"
>
Troubleshooting notifications
</h4>
<p>
Not receiving notifications? Start by sending a test notification to all your devices to check if theyre working as expected. If issues persist, explore ways to solve them with troubleshooting steps.
</p>
<div
class="sectionNoticeActions"
>
<button
class="btn btn-sm sectionNoticeButton btn-primary"
>
Send a test notification
</button>
<button
class="btn btn-sm sectionNoticeButton btn-tertiary"
>
Troubleshooting docs
<i
class="icon icon-open-in-new"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>,

Просмотреть файл

@@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {sendTestNotification} from 'actions/notification_actions';
import {act, renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
import SendTestNotificationNotice from './send_test_notification_notice';
jest.mock('actions/notification_actions', () => ({
sendTestNotification: jest.fn().mockResolvedValue({status: 'OK'}),
}));
const mockedSendTestNotification = jest.mocked(sendTestNotification);
describe('components/user_settings/notifications/send_test_notification_notice', () => {
jest.useFakeTimers();
it('should match snapshot', () => {
const {container} = renderWithContext((<SendTestNotificationNotice/>));
expect(container).toMatchSnapshot();
});
it('should not show on admin mode', () => {
const {container} = renderWithContext((<SendTestNotificationNotice adminMode={true}/>));
expect(container).toBeEmptyDOMElement();
});
it('should send the notificaton when the send button is clicked', async () => {
renderWithContext((<SendTestNotificationNotice/>));
expect(mockedSendTestNotification).not.toHaveBeenCalled();
act(() => screen.getByText('Send a test notification').click());
await waitFor(() => {
expect(mockedSendTestNotification).toHaveBeenCalled();
expect(screen.getByText('Test notification sent')).toBeInTheDocument();
});
});
it('should open link when the secondary button is clicked', () => {
const originalOpen = window.open;
const mockedOpen = jest.fn();
window.open = mockedOpen;
renderWithContext((<SendTestNotificationNotice/>));
expect(mockedOpen).not.toHaveBeenCalled();
act(() => screen.getByText('Troubleshooting docs').click());
expect(mockedOpen).toHaveBeenCalled();
window.open = originalOpen;
});
it('should show error on button when the system returns an error', async () => {
mockedSendTestNotification.mockResolvedValueOnce({status: 'NOT OK'});
const originalConsole = console.error;
const mockedConsole = jest.fn();
console.error = mockedConsole;
renderWithContext((<SendTestNotificationNotice/>));
expect(mockedSendTestNotification).not.toHaveBeenCalled();
act(() => screen.getByText('Send a test notification').click());
await waitFor(() => {
expect(mockedSendTestNotification).toHaveBeenCalled();
expect(screen.getByText('Error sending test notification')).toBeInTheDocument();
expect(mockedConsole).toHaveBeenCalledWith({status: 'NOT OK'});
});
console.error = originalConsole;
});
});

Просмотреть файл

@@ -0,0 +1,138 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {sendTestNotification} from 'actions/notification_actions';
import {useExternalLink} from 'components/common/hooks/use_external_link';
import SectionNotice from 'components/section_notice';
const sectionNoticeContainerStyle: React.CSSProperties = {marginTop: 20};
const TIME_TO_SENDING = 500;
const TIME_TO_SEND = 500;
const TIME_TO_IDLE = 3000;
type Props = {
adminMode?: boolean;
};
type ButtonState = 'idle'|'sending'|'sent'|'error';
const SendTestNotificationNotice = ({
adminMode = false,
}: Props) => {
const intl = useIntl();
const [buttonState, setButtonState] = useState<ButtonState>('idle');
const isSending = useRef(false);
const timeout = useRef<NodeJS.Timeout>();
const [externalLink] = useExternalLink('https://mattermost.com/pl/troubleshoot-notifications');
const onGoToNotificationDocumentation = useCallback(() => {
window.open(externalLink);
}, [externalLink]);
const onSendTestNotificationClick = useCallback(async () => {
if (isSending.current) {
return;
}
isSending.current = true;
let isShowingSending = false;
timeout.current = setTimeout(() => {
isShowingSending = true;
setButtonState('sending');
}, TIME_TO_SENDING);
const result = await sendTestNotification();
clearTimeout(timeout.current);
const setResult = () => {
if (result.status === 'OK') {
setButtonState('sent');
} else {
// We want to log this error into the console mainly
// for debugging reasons. We still use the 'error' level
// because it is an unexpected error.
// eslint-disable-next-line no-console
console.error(result);
setButtonState('error');
}
timeout.current = setTimeout(() => {
isSending.current = false;
setButtonState('idle');
}, TIME_TO_IDLE);
};
if (isShowingSending) {
timeout.current = setTimeout(setResult, TIME_TO_SEND);
} else {
setResult();
}
}, []);
useEffect(() => {
return () => {
clearTimeout(timeout.current);
};
}, []);
const primaryButton = useMemo(() => {
let text;
let icon;
let loading;
switch (buttonState) {
case 'idle':
text = intl.formatMessage({id: 'user_settings.notifications.test_notification.send_button.send', defaultMessage: 'Send a test notification'});
break;
case 'sending':
text = intl.formatMessage({id: 'user_settings.notifications.test_notification.send_button.sending', defaultMessage: 'Sending a test notification'});
loading = true;
break;
case 'sent':
text = intl.formatMessage({id: 'user_settings.notifications.test_notification.send_button.sent', defaultMessage: 'Test notification sent'});
icon = 'icon-check';
break;
case 'error':
text = intl.formatMessage({id: 'user_settings.notifications.test_notification.send_button.error', defaultMessage: 'Error sending test notification'});
icon = 'icon-alert-outline';
}
return {
onClick: onSendTestNotificationClick,
text,
leadingIcon: icon,
loading,
};
}, [buttonState, intl, onSendTestNotificationClick]);
const secondaryButton = useMemo(() => {
return {
onClick: onGoToNotificationDocumentation,
text: intl.formatMessage({id: 'user_settings.notifications.test_notification.go_to_docs', defaultMessage: 'Troubleshooting docs'}),
trailingIcon: 'icon-open-in-new',
};
}, [intl, onGoToNotificationDocumentation]);
if (adminMode) {
return null;
}
return (
<>
<div className='divider-light'/>
<div style={sectionNoticeContainerStyle}>
<SectionNotice
text={intl.formatMessage({
id: 'user_settings.notifications.test_notification.body',
defaultMessage: 'Not receiving notifications? Start by sending a test notification to all your devices to check if theyre working as expected. If issues persist, explore ways to solve them with troubleshooting steps.',
})}
title={intl.formatMessage({id: 'user_settings.notifications.test_notification.title', defaultMessage: 'Troubleshooting notifications'})}
primaryButton={primaryButton}
tertiaryButton={secondaryButton}
type='hint'
/>
</div>
</>
);
};
export default SendTestNotificationNotice;

Просмотреть файл

@@ -31,6 +31,7 @@ import DesktopAndMobileNotificationSettings from './desktop_and_mobile_notificat
import DesktopNotificationSoundsSettings from './desktop_notification_sounds_setting';
import EmailNotificationSetting from './email_notification_setting';
import ManageAutoResponder from './manage_auto_responder/manage_auto_responder';
import SendTestNotificationNotice from './send_test_notification_notice';
import SettingDesktopHeader from '../headers/setting_desktop_header';
import SettingMobileHeader from '../headers/setting_mobile_header';
@@ -1099,7 +1100,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
{keywordsWithHighlightSection}
</>
)}
<div className='divider-dark'/>
<SendTestNotificationNotice adminMode={this.props.adminMode}/>
</div>
</div>

Просмотреть файл

@@ -5484,6 +5484,13 @@
"user_profile.roleTitle.team_admin": "Team Admin",
"user_profile.send.dm": "Message",
"user_profile.send.dm.yourself": "Send yourself a message",
"user_settings.notifications.test_notification.body": "Not receiving notifications? Start by sending a test notification to all your devices to check if theyre working as expected. If issues persist, explore ways to solve them with troubleshooting steps.",
"user_settings.notifications.test_notification.go_to_docs": "Troubleshooting docs",
"user_settings.notifications.test_notification.send_button.error": "Error sending test notification",
"user_settings.notifications.test_notification.send_button.send": "Send a test notification",
"user_settings.notifications.test_notification.send_button.sending": "Sending a test notification",
"user_settings.notifications.test_notification.send_button.sent": "Test notification sent",
"user_settings.notifications.test_notification.title": "Troubleshooting notifications",
"user.settings.advance.confirmDeactivateAccountTitle": "Confirm Deactivation",
"user.settings.advance.confirmDeactivateDesc": "Are you sure you want to deactivate your account? This can only be reversed by your System Administrator.",
"user.settings.advance.deactivate_member_modal.deactivateButton": "Yes, deactivate my account",

Просмотреть файл

@@ -3292,6 +3292,13 @@ export default class Client4 {
);
};
sendTestNotificaiton = () => {
return this.doFetch<StatusOK>(
`${this.getBaseRoute()}/notifications/test`,
{method: 'post'},
);
};
testEmail = (config?: AdminConfig) => {
return this.doFetch<StatusOK>(
`${this.getBaseRoute()}/email/test`,