Add test notification tool (#28334)
* 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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
30a6ddc995
Коммит
118d0346ee
@@ -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);
|
||||
});
|
||||
});
|
||||
47
webapp/channels/src/components/common/hooks/use_external_link.ts
Обычный файл
47
webapp/channels/src/components/common/hooks/use_external_link.ts
Обычный файл
@@ -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
поставляемый
Обычный файл
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 they’re 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 they’re 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 they’re 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 they’re 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 they’re 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 they’re 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 they’re 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 they’re 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 they’re 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`,
|
||||
|
||||
Ссылка в новой задаче
Block a user