[MM-53102] Add support for multi-word highlights without notifications in web (#24050)

- Adds a new section under settings/notifications for adding custom multi-word keywords that get highlighted without notification
- Adds a new classname for highlighting words although the styling is the same as mentions highlights
- Added a few components to the ReduxFromProps pattern
- Adds supported type for the hook of PluginComponent type
- Add upsell for highlight without notification
- Moved 'setting_item.tsx' to the components folder
- Improved prop names and function structure for setting_item, setting_item_max and setting_item_min
- Moved 'toggle_modal_button.tsx' to the components folder
- Removed t and utility messages from a few components
- Fixed bug where the tooltip was not getting rendered on restrictedButtons
- Improved the mobile view of the settings modal
- Adds E2E for the feature
This commit is contained in:
M-ZubairAhmed 2023-11-11 19:33:28 +05:30 committed by GitHub
parent 48bf4e9bd8
commit 9ac389f506
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
101 changed files with 3145 additions and 1477 deletions

View File

@ -34,8 +34,8 @@ describe('Verify Accessibility Support in different sections in Settings and Pro
{key: 'desktop', label: 'Desktop Notifications', type: 'radio'},
{key: 'email', label: 'Email Notifications', type: 'radio'},
{key: 'push', label: 'Mobile Push Notifications', type: 'radio'},
{key: 'keysWithNotification', label: 'Keywords that trigger Notifications', type: 'checkbox'},
{key: 'comments', label: 'Reply notifications', type: 'radio'},
{key: 'keysWithNotification', label: 'Keywords That Trigger Notifications', type: 'checkbox'},
{key: 'comments', label: 'Reply Notifications', type: 'radio'},
],
display: [
{key: 'theme', label: 'Theme', type: 'radio'},

View File

@ -188,6 +188,8 @@ function mapFeatureIdToId(id: string) {
return 'All Professional features';
case 'mattermost.feature.all_enterprise':
return 'All Enterprise features';
case 'mattermost.feature.highlight_without_notification':
return 'Keywords Highlight Without Notification';
default:
return '';
}

View File

@ -48,7 +48,7 @@ describe('Auto Response In DMs', () => {
// # Open 'Settings' modal and view 'Notifications'
cy.uiOpenSettingsModal().within(() => {
// # Click on 'Edit' for 'Automatic Direct Message Replies
cy.get('#auto-responderEdit').should('be.visible').click();
cy.get('#auto-responderEdit').should('exist').scrollIntoView().and('be.visible').click();
// # Click on 'Enabled' checkbox
cy.get('#autoResponderActive').should('be.visible').click();

View File

@ -23,8 +23,8 @@ describe('Notifications', () => {
// # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => {
// # Open 'Keywords that trigger Notifications' setting and uncheck all the checkboxes
cy.findByRole('heading', {name: 'Keywords that trigger Notifications'}).should('be.visible').click();
// # Open 'Keywords That Trigger Notifications' setting and uncheck all the checkboxes
cy.findByRole('heading', {name: 'Keywords That Trigger Notifications'}).should('be.visible').click();
cy.findByRole('checkbox', {name: `Your case-sensitive first name "${otherUser.first_name}"`}).should('not.be.checked');
cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked');
cy.findByRole('checkbox', {name: 'Channel-wide mentions "@channel", "@all", "@here"'}).click().should('not.be.checked');

View File

@ -252,7 +252,7 @@ function setNotificationSettings(desiredSettings = {first: true, username: true,
cy.findAllByText('Notifications').should('be.visible');
// Open up 'Words that trigger mentions' sub-section
cy.findByText('Keywords that trigger Notifications').
cy.findByText('Keywords That Trigger Notifications').
scrollIntoView().
click();

View File

@ -29,8 +29,8 @@ describe('Notifications', () => {
// # Open 'Settings' modal
cy.uiOpenSettingsModal().within(() => {
// # Open 'Keywords that trigger Notifications' setting
cy.findByRole('heading', {name: 'Keywords that trigger Notifications'}).should('be.visible').click();
// # Open 'Keywords That Trigger Notifications' setting
cy.findByRole('heading', {name: 'Keywords That Trigger Notifications'}).should('be.visible').click();
// * As otherUser, ensure that 'Your non-case sensitive username' is not checked
cy.findByRole('checkbox', {name: `Your non case-sensitive username "${otherUser.username}"`}).should('not.be.checked');

View File

@ -9,7 +9,7 @@ export default class DeletePostModal {
constructor(container: Locator) {
this.container = container;
this.confirmButton = this.container.locator('#deletePostModalButton');
this.confirmButton = container.locator('#deletePostModalButton');
}
async toBeVisible() {

View File

@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
type NotificationSettingsSection = 'keysWithHighlight' | 'keysWithNotification';
export default class NotificationsSettings {
readonly container: Locator;
constructor(container: Locator) {
this.container = container;
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async expandSection(section: NotificationSettingsSection) {
if (section === 'keysWithHighlight') {
await this.container.getByText('Keywords That Get Highlighted (without notifications)').click();
await this.verifySectionIsExpanded('keysWithHighlight');
}
}
async verifySectionIsExpanded(section: NotificationSettingsSection) {
await expect(this.container.locator(`#${section}Edit`)).not.toBeVisible();
if (section === 'keysWithHighlight') {
await expect(
this.container.getByText(
'Enter non case-sensitive keywords, press Tab or use commas to separate them:',
),
).toBeVisible();
await expect(
this.container.getByText(
'These keywords will be shown to you with a highlight when anyone sends a message that includes them.',
),
).toBeVisible();
}
}
async getKeywordsInput() {
await expect(this.container.locator('input')).toBeVisible();
return this.container.locator('input');
}
async save() {
await expect(this.container.getByText('Save')).toBeVisible();
await this.container.getByText('Save').click();
}
}
export {NotificationsSettings};

View File

@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
import {NotificationsSettings} from './notification_settings';
export default class SettingsModal {
readonly container: Locator;
readonly notificationsSettingsTab;
readonly notificationsSettings;
constructor(container: Locator) {
this.container = container;
this.notificationsSettingsTab = container.locator('#notificationsButton');
this.notificationsSettings = new NotificationsSettings(container.locator('#notificationSettings'));
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
async openNotificationsTab() {
await expect(this.notificationsSettingsTab).toBeVisible();
await this.notificationsSettingsTab.click();
await this.notificationsSettings.toBeVisible();
}
async closeModal() {
await this.container.getByLabel('Close').click();
await expect(this.container).not.toBeVisible();
}
}
export {SettingsModal};

View File

@ -7,11 +7,19 @@ export default class GlobalHeader {
readonly container: Locator;
readonly productSwitchMenu;
readonly recentMentionsButton;
readonly settingsButton;
constructor(container: Locator) {
this.container = container;
this.productSwitchMenu = container.getByRole('button', {name: 'Product switch menu'});
this.recentMentionsButton = container.getByRole('button', {name: 'Recent mentions'});
this.settingsButton = container.getByRole('button', {name: 'Settings'});
}
async toBeVisible(name: string) {
await expect(this.container.getByRole('heading', {name})).toBeVisible();
}
async switchProduct(name: string) {
@ -19,8 +27,14 @@ export default class GlobalHeader {
await this.container.getByRole('link', {name}).click();
}
async toBeVisible(name: string) {
await expect(this.container.getByRole('heading', {name})).toBeVisible();
async openSettings() {
await expect(this.settingsButton).toBeVisible();
await this.settingsButton.click();
}
async openRecentMentions() {
await expect(this.recentMentionsButton).toBeVisible();
await this.recentMentionsButton.click();
}
}

View File

@ -10,6 +10,7 @@ import {ChannelsSidebarLeft} from './channels/sidebar_left';
import {ChannelsSidebarRight} from './channels/sidebar_right';
import {DeletePostModal} from './channels/delete_post_modal';
import {FindChannelsModal} from './channels/find_channels_modal';
import {SettingsModal} from './channels/settings/settings_modal';
import {Footer} from './footer';
import {GlobalHeader} from './global_header';
import {MainHeader} from './main_header';
@ -30,6 +31,7 @@ const components = {
ChannelsPost,
FindChannelsModal,
DeletePostModal,
SettingsModal,
PostDotMenu,
PostMenu,
ThreadFooter,

View File

@ -18,6 +18,7 @@ export default class ChannelsPage {
readonly findChannelsModal;
readonly deletePostModal;
readonly settingsModal;
readonly postDotMenu;
readonly postReminderMenu;
@ -37,6 +38,7 @@ export default class ChannelsPage {
// Modals
this.findChannelsModal = new components.FindChannelsModal(page.getByRole('dialog', {name: 'Find Channels'}));
this.deletePostModal = new components.DeletePostModal(page.locator('#deletePostModal'));
this.settingsModal = new components.SettingsModal(page.getByRole('dialog', {name: 'Settings'}));
// Menus
this.postDotMenu = new components.PostDotMenu(page.getByRole('menu', {name: 'Post extra options'}));

View File

@ -0,0 +1,271 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect} from '@playwright/test';
import {test} from '@e2e-support/test_fixture';
import {getRandomId} from '@e2e-support/util';
import {createRandomPost} from '@e2e-support/server/post';
const keywords = [`AB${getRandomId()}`, `CD${getRandomId()}`, `EF${getRandomId()}`, `Highlight me ${getRandomId()}`];
const highlightWithoutNotificationClass = 'non-notification-highlight';
test('MM-T5465-1 Should add the keyword when enter, comma or tab is pressed on the textbox', async ({pw, pages}) => {
const {user} = await pw.initSetup();
// # Log in as a user in new browser context
const {page} = await pw.testBrowser.login(user);
// # Visit default channel page
const channelPage = new pages.ChannelsPage(page);
await channelPage.goto();
await channelPage.toBeVisible();
await channelPage.centerView.postCreate.postMessage('Hello World');
// # Open settings modal
await channelPage.globalHeader.openSettings();
await channelPage.settingsModal.toBeVisible();
// # Open notifications tab
await channelPage.settingsModal.openNotificationsTab();
// # Open keywords that get highlighted section
await channelPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
const keywordsInput = await channelPage.settingsModal.notificationsSettings.getKeywordsInput();
// # Enter keyword 1
await keywordsInput.type(keywords[0]);
// # Press Comma on the textbox
await keywordsInput.press(',');
// # Enter keyword 2
await keywordsInput.type(keywords[1]);
// # Press Tab on the textbox
await keywordsInput.press('Tab');
// # Enter keyword 3
await keywordsInput.type(keywords[2]);
// # Press Enter on the textbox
await keywordsInput.press('Enter');
// * Verify that the keywords have been added to the collapsed description
await expect(channelPage.settingsModal.notificationsSettings.container.getByText(keywords[0])).toBeVisible();
await expect(channelPage.settingsModal.notificationsSettings.container.getByText(keywords[1])).toBeVisible();
await expect(channelPage.settingsModal.notificationsSettings.container.getByText(keywords[2])).toBeVisible();
});
test('MM-T5465-2 Should highlight the keywords when a message is sent with the keyword in center', async ({
pw,
pages,
}) => {
const {user} = await pw.initSetup();
// # Log in as a user in new browser context
const {page} = await pw.testBrowser.login(user);
// # Visit default channel page
const channelPage = new pages.ChannelsPage(page);
await channelPage.goto();
await channelPage.toBeVisible();
// # Open settings modal
await channelPage.globalHeader.openSettings();
await channelPage.settingsModal.toBeVisible();
// # Open notifications tab
await channelPage.settingsModal.openNotificationsTab();
// # Open keywords that get highlighted section
await channelPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
// # Enter the keyword
const keywordsInput = await channelPage.settingsModal.notificationsSettings.getKeywordsInput();
await keywordsInput.type(keywords[3]);
await keywordsInput.press('Tab');
// # Save the keyword
await channelPage.settingsModal.notificationsSettings.save();
// # Close the settings modal
await channelPage.settingsModal.closeModal();
// # Post a message without the keyword
const messageWithoutKeyword = 'This message does not contain the keyword';
await channelPage.centerView.postCreate.postMessage(messageWithoutKeyword);
const lastPostWithoutHighlight = await channelPage.centerView.getLastPost();
// * Verify that the keywords are not highlighted
await expect(lastPostWithoutHighlight.container.getByText(messageWithoutKeyword)).toBeVisible();
await expect(lastPostWithoutHighlight.container.getByText(messageWithoutKeyword)).not.toHaveClass(
highlightWithoutNotificationClass,
);
// # Post a message with the keyword
const messageWithKeyword = `This message contains the keyword ${keywords[3]}`;
await channelPage.centerView.postCreate.postMessage(messageWithKeyword);
const lastPostWithHighlight = await channelPage.centerView.getLastPost();
// * Verify that the keywords are highlighted
await expect(lastPostWithHighlight.container.getByText(messageWithKeyword)).toBeVisible();
await expect(lastPostWithHighlight.container.getByText(keywords[3])).toHaveClass(highlightWithoutNotificationClass);
});
test('MM-T5465-3 Should highlight the keywords when a message is sent with the keyword in rhs', async ({pw, pages}) => {
const {user} = await pw.initSetup();
// # Log in as a user in new browser context
const {page} = await pw.testBrowser.login(user);
// # Visit default channel page
const channelPage = new pages.ChannelsPage(page);
await channelPage.goto();
await channelPage.toBeVisible();
// # Open settings modal
await channelPage.globalHeader.openSettings();
await channelPage.settingsModal.toBeVisible();
// # Open notifications tab
await channelPage.settingsModal.openNotificationsTab();
// # Open keywords that get highlighted section
await channelPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
// # Enter the keyword
const keywordsInput = await channelPage.settingsModal.notificationsSettings.getKeywordsInput();
await keywordsInput.type(keywords[3]);
await keywordsInput.press('Tab');
// # Save the keyword
await channelPage.settingsModal.notificationsSettings.save();
// # Close the settings modal
await channelPage.settingsModal.closeModal();
// # Post a message without the keyword
const messageWithoutKeyword = 'This message does not contain the keyword';
await channelPage.centerView.postCreate.postMessage(messageWithoutKeyword);
const lastPostWithoutHighlight = await channelPage.centerView.getLastPost();
// # Open the message in the RHS
await lastPostWithoutHighlight.hover();
await lastPostWithoutHighlight.postMenu.toBeVisible();
await lastPostWithoutHighlight.postMenu.reply();
await channelPage.sidebarRight.toBeVisible();
// # Post a message with the keyword in the RHS
const messageWithKeyword = `This message contains the keyword ${keywords[3]}`;
await channelPage.sidebarRight.postCreate.postMessage(messageWithKeyword);
// * Verify that the keywords are highlighted
const lastPostWithHighlightInRHS = await channelPage.sidebarRight.getLastPost();
await expect(lastPostWithHighlightInRHS.container.getByText(messageWithKeyword)).toBeVisible();
await expect(lastPostWithHighlightInRHS.container.getByText(keywords[3])).toHaveClass(
highlightWithoutNotificationClass,
);
});
test('MM-T5465-4 Highlighted keywords should not appear in the Recent Mentions', async ({pw, pages}) => {
const {user} = await pw.initSetup();
// # Log in as a user in new browser context
const {page} = await pw.testBrowser.login(user);
// # Visit default channel page
const channelPage = new pages.ChannelsPage(page);
await channelPage.goto();
await channelPage.toBeVisible();
// # Open settings modal
await channelPage.globalHeader.openSettings();
await channelPage.settingsModal.toBeVisible();
// # Open notifications tab
await channelPage.settingsModal.openNotificationsTab();
// # Open keywords that get highlighted section
await channelPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
// # Enter the keyword
const keywordsInput = await channelPage.settingsModal.notificationsSettings.getKeywordsInput();
await keywordsInput.type(keywords[0]);
await keywordsInput.press('Tab');
// # Save the keyword
await channelPage.settingsModal.notificationsSettings.save();
// # Close the settings modal
await channelPage.settingsModal.closeModal();
// # Open the recent mentions
await channelPage.globalHeader.openRecentMentions();
// * Verify recent mentions is empty
await channelPage.sidebarRight.toBeVisible();
await expect(channelPage.sidebarRight.container.getByText('No mentions yet')).toBeVisible();
});
test('MM-T5465-5 Should highlight keywords in message sent from another user', async ({pw, pages}) => {
const {adminClient, team, adminUser, user} = await pw.initSetup();
if (!adminUser) {
throw new Error('Failed to create admin user');
}
// # Get the default channel of the team for getting the channel id
const channel = await adminClient.getChannelByName(team.id, 'town-square');
const highlightKeyword = keywords[0];
const messageWithKeyword = `This recieved message contains the ${highlightKeyword} keyword `;
// # Create a post containing the keyword in the channel by admin
await adminClient.createPost(
createRandomPost({
message: messageWithKeyword,
channel_id: channel.id,
user_id: adminUser.id,
}),
);
// # Now log in as a user in new browser context
const {page} = await pw.testBrowser.login(user);
// # Visit default channel page
const channelPage = new pages.ChannelsPage(page);
await channelPage.goto();
await channelPage.toBeVisible();
// # Open settings modal
await channelPage.globalHeader.openSettings();
await channelPage.settingsModal.toBeVisible();
// # Open notifications tab
await channelPage.settingsModal.openNotificationsTab();
// # Open keywords that get highlighted section
await channelPage.settingsModal.notificationsSettings.expandSection('keysWithHighlight');
// # Enter the keyword
const keywordsInput = await channelPage.settingsModal.notificationsSettings.getKeywordsInput();
await keywordsInput.type(keywords[0]);
await keywordsInput.press('Tab');
// # Save the keyword
await channelPage.settingsModal.notificationsSettings.save();
// # Close the settings modal
await channelPage.settingsModal.closeModal();
// * Verify that the keywords are highlighted in the last message recieved
const lastPostWithHighlight = await channelPage.centerView.getLastPost();
await expect(lastPostWithHighlight.container.getByText(messageWithKeyword)).toBeVisible();
await expect(lastPostWithHighlight.container.getByText(highlightKeyword)).toHaveClass(
highlightWithoutNotificationClass,
);
});

View File

@ -13,17 +13,18 @@ import (
type MattermostFeature string
const (
PaidFeatureGuestAccounts = MattermostFeature("mattermost.feature.guest_accounts")
PaidFeatureCustomUsergroups = MattermostFeature("mattermost.feature.custom_user_groups")
PaidFeatureCreateMultipleTeams = MattermostFeature("mattermost.feature.create_multiple_teams")
PaidFeatureStartcall = MattermostFeature("mattermost.feature.start_call")
PaidFeaturePlaybooksRetrospective = MattermostFeature("mattermost.feature.playbooks_retro")
PaidFeatureUnlimitedMessages = MattermostFeature("mattermost.feature.unlimited_messages")
PaidFeatureUnlimitedFileStorage = MattermostFeature("mattermost.feature.unlimited_file_storage")
PaidFeatureAllProfessionalfeatures = MattermostFeature("mattermost.feature.all_professional")
PaidFeatureAllEnterprisefeatures = MattermostFeature("mattermost.feature.all_enterprise")
UpgradeDowngradedWorkspace = MattermostFeature("mattermost.feature.upgrade_downgraded_workspace")
PluginFeature = MattermostFeature("mattermost.feature.plugin")
PaidFeatureGuestAccounts = MattermostFeature("mattermost.feature.guest_accounts")
PaidFeatureCustomUsergroups = MattermostFeature("mattermost.feature.custom_user_groups")
PaidFeatureCreateMultipleTeams = MattermostFeature("mattermost.feature.create_multiple_teams")
PaidFeatureStartcall = MattermostFeature("mattermost.feature.start_call")
PaidFeaturePlaybooksRetrospective = MattermostFeature("mattermost.feature.playbooks_retro")
PaidFeatureUnlimitedMessages = MattermostFeature("mattermost.feature.unlimited_messages")
PaidFeatureUnlimitedFileStorage = MattermostFeature("mattermost.feature.unlimited_file_storage")
PaidFeatureAllProfessionalfeatures = MattermostFeature("mattermost.feature.all_professional")
PaidFeatureAllEnterprisefeatures = MattermostFeature("mattermost.feature.all_enterprise")
UpgradeDowngradedWorkspace = MattermostFeature("mattermost.feature.upgrade_downgraded_workspace")
PluginFeature = MattermostFeature("mattermost.feature.plugin")
PaidFeatureHighlightWithoutNotification = MattermostFeature("mattermost.feature.highlight_without_notification")
)
var validSKUs = map[string]struct{}{
@ -33,16 +34,17 @@ var validSKUs = map[string]struct{}{
// These are the features a non admin would typically ping an admin about
var paidFeatures = map[MattermostFeature]struct{}{
PaidFeatureGuestAccounts: {},
PaidFeatureCustomUsergroups: {},
PaidFeatureCreateMultipleTeams: {},
PaidFeatureStartcall: {},
PaidFeaturePlaybooksRetrospective: {},
PaidFeatureUnlimitedMessages: {},
PaidFeatureUnlimitedFileStorage: {},
PaidFeatureAllProfessionalfeatures: {},
PaidFeatureAllEnterprisefeatures: {},
UpgradeDowngradedWorkspace: {},
PaidFeatureGuestAccounts: {},
PaidFeatureCustomUsergroups: {},
PaidFeatureCreateMultipleTeams: {},
PaidFeatureStartcall: {},
PaidFeaturePlaybooksRetrospective: {},
PaidFeatureUnlimitedMessages: {},
PaidFeatureUnlimitedFileStorage: {},
PaidFeatureAllProfessionalfeatures: {},
PaidFeatureAllEnterprisefeatures: {},
UpgradeDowngradedWorkspace: {},
PaidFeatureHighlightWithoutNotification: {},
}
type NotifyAdminToUpgradeRequest struct {

View File

@ -36,6 +36,7 @@ const (
ChannelMentionsNotifyProp = "channel"
CommentsNotifyProp = "comments"
MentionKeysNotifyProp = "mention_keys"
HighlightsNotifyProp = "highlight_keys"
CommentsNotifyNever = "never"
CommentsNotifyRoot = "root"
CommentsNotifyAny = "any"

View File

@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/SettingItemMin should match snapshot 1`] = `
<div
className="section-min"
onClick={[Function]}
>
<div
className="secion-min__header"
>
<h4
className="section-min__title"
id="sectionTitle"
>
title
</h4>
<button
aria-expanded={false}
aria-labelledby="sectionTitle sectionEdit"
className="color--link style--none section-min__edit"
id="sectionEdit"
onClick={[Function]}
>
<EditIcon />
<MemoizedFormattedMessage
defaultMessage="Edit"
id="setting_item_min.edit"
/>
</button>
</div>
<div
className="section-min__describe"
id="sectionDesc"
>
describe
</div>
</div>
`;
exports[`components/SettingItemMin should match snapshot, on disableOpen to true 1`] = `
<div
className="section-min"
onClick={[Function]}
>
<div
className="secion-min__header"
>
<h4
className="section-min__title"
id="sectionTitle"
>
title
</h4>
<button
aria-expanded={false}
aria-labelledby="sectionTitle sectionEdit"
className="color--link style--none section-min__edit"
id="sectionEdit"
onClick={[Function]}
>
<EditIcon />
<MemoizedFormattedMessage
defaultMessage="Edit"
id="setting_item_min.edit"
/>
</button>
</div>
<div
className="section-min__describe"
id="sectionDesc"
>
describe
</div>
</div>
`;

View File

@ -18,7 +18,6 @@ exports[`components/TextBox should match snapshot with additional, optional prop
"hideUtilities": true,
}
}
mentionKeys={Array []}
message="some test text"
/>
</div>
@ -147,7 +146,6 @@ exports[`components/TextBox should match snapshot with required props 1`] = `
"hideUtilities": true,
}
}
mentionKeys={Array []}
message="some test text"
/>
</div>
@ -271,7 +269,6 @@ exports[`components/TextBox should throw error when new property is too long 1`]
"hideUtilities": true,
}
}
mentionKeys={Array []}
message="some test text that exceeds char limit"
/>
</div>
@ -395,7 +392,6 @@ exports[`components/TextBox should throw error when value is too long 1`] = `
"hideUtilities": true,
}
}
mentionKeys={Array []}
message="some test text that exceeds char limit"
/>
</div>

View File

@ -2,11 +2,6 @@
exports[`components/ToggleModalButton component should match snapshot 1`] = `
<ToggleModalButton
actions={
Object {
"openModal": [Function],
}
}
ariaLabel="Delete Channel"
dialogType={[Function]}
id="channelDelete"

View File

@ -62,6 +62,7 @@ exports[`components/UserList should match default snapshot when there are users
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -122,6 +123,7 @@ exports[`components/UserList should match default snapshot when there are users
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -111,6 +111,7 @@ exports[`components/admin_console/add_users_to_team_modal/AddUsersToTeamModal sh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -151,6 +152,7 @@ exports[`components/admin_console/add_users_to_team_modal/AddUsersToTeamModal sh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -297,6 +299,7 @@ exports[`components/admin_console/add_users_to_team_modal/AddUsersToTeamModal sh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -337,6 +340,7 @@ exports[`components/admin_console/add_users_to_team_modal/AddUsersToTeamModal sh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -59,6 +59,7 @@ exports[`admin_console/team_channel_settings/group/GroupList should match snapsh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -106,6 +107,7 @@ exports[`admin_console/team_channel_settings/group/GroupList should match snapsh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -153,6 +155,7 @@ exports[`admin_console/team_channel_settings/group/GroupList should match snapsh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -200,6 +203,7 @@ exports[`admin_console/team_channel_settings/group/GroupList should match snapsh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -247,6 +251,7 @@ exports[`admin_console/team_channel_settings/group/GroupList should match snapsh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -294,6 +299,7 @@ exports[`admin_console/team_channel_settings/group/GroupList should match snapsh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -341,6 +347,7 @@ exports[`admin_console/team_channel_settings/group/GroupList should match snapsh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -388,6 +395,7 @@ exports[`admin_console/team_channel_settings/group/GroupList should match snapsh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -435,6 +443,7 @@ exports[`admin_console/team_channel_settings/group/GroupList should match snapsh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -482,6 +491,7 @@ exports[`admin_console/team_channel_settings/group/GroupList should match snapsh
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -26,6 +26,7 @@ describe('components/admin_console/reset_password_modal/reset_password_modal.tsx
first_name: 'true',
mark_unread: 'all',
mention_keys: '',
highlight_keys: '',
push: 'default',
push_status: 'ooo',
};

View File

@ -114,6 +114,7 @@ exports[`admin_console/add_users_to_role_modal search should not include bot use
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -359,6 +360,7 @@ exports[`admin_console/add_users_to_role_modal should have single passed value 1
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -503,6 +505,7 @@ exports[`admin_console/add_users_to_role_modal should include additional user 1`
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -543,6 +546,7 @@ exports[`admin_console/add_users_to_role_modal should include additional user 1`
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -687,6 +691,7 @@ exports[`admin_console/add_users_to_role_modal should include additional user 2`
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -727,6 +732,7 @@ exports[`admin_console/add_users_to_role_modal should include additional user 2`
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -871,6 +877,7 @@ exports[`admin_console/add_users_to_role_modal should not include bot user 1`] =
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -3,7 +3,7 @@
exports[`admin_console/system_role_users should match snapshot 1`] = `
<AdminPanel
button={
<Memo(Connect(ToggleModalButton))
<ToggleModalButton
className="btn btn-primary"
dialogProps={
Object {
@ -32,6 +32,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -72,6 +73,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -119,7 +121,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
defaultMessage="Add People"
id="admin.permissions.system_role_users.add_people"
/>
</Memo(Connect(ToggleModalButton))>
</ToggleModalButton>
}
className=""
id="SystemRoleUsers"
@ -191,6 +193,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -236,6 +239,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -283,6 +287,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -328,6 +333,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -359,7 +365,7 @@ exports[`admin_console/system_role_users should match snapshot 1`] = `
exports[`admin_console/system_role_users should match snapshot with readOnly true 1`] = `
<AdminPanel
button={
<Memo(Connect(ToggleModalButton))
<ToggleModalButton
className="btn btn-primary"
dialogProps={
Object {
@ -388,6 +394,7 @@ exports[`admin_console/system_role_users should match snapshot with readOnly tru
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -428,6 +435,7 @@ exports[`admin_console/system_role_users should match snapshot with readOnly tru
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -475,7 +483,7 @@ exports[`admin_console/system_role_users should match snapshot with readOnly tru
defaultMessage="Add People"
id="admin.permissions.system_role_users.add_people"
/>
</Memo(Connect(ToggleModalButton))>
</ToggleModalButton>
}
className=""
id="SystemRoleUsers"
@ -547,6 +555,7 @@ exports[`admin_console/system_role_users should match snapshot with readOnly tru
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -592,6 +601,7 @@ exports[`admin_console/system_role_users should match snapshot with readOnly tru
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -639,6 +649,7 @@ exports[`admin_console/system_role_users should match snapshot with readOnly tru
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -684,6 +695,7 @@ exports[`admin_console/system_role_users should match snapshot with readOnly tru
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -3,7 +3,7 @@
exports[`admin_console/team_channel_settings/channel/ChannelGroups should match snapshot 1`] = `
<AdminPanel
button={
<Memo(Connect(ToggleModalButton))
<ToggleModalButton
className="btn btn-primary"
dialogProps={
Object {
@ -42,7 +42,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelGroups should match
defaultMessage="Add Group"
id="admin.channel_settings.channel_details.add_group"
/>
</Memo(Connect(ToggleModalButton))>
</ToggleModalButton>
}
className=""
id="channel_groups"

View File

@ -3,7 +3,7 @@
exports[`admin_console/team_channel_settings/channel/ChannelMembers should match snapshot 1`] = `
<AdminPanel
button={
<Memo(Connect(ToggleModalButton))
<ToggleModalButton
className="btn btn-primary"
dialogProps={
Object {
@ -47,7 +47,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
defaultMessage="Add Members"
id="admin.team_settings.team_details.add_members"
/>
</Memo(Connect(ToggleModalButton))>
</ToggleModalButton>
}
className=""
id="channelMembers"
@ -215,6 +215,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -253,6 +254,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -291,6 +293,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -314,7 +317,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
exports[`admin_console/team_channel_settings/channel/ChannelMembers should match snapshot loading no users 1`] = `
<AdminPanel
button={
<Memo(Connect(ToggleModalButton))
<ToggleModalButton
className="btn btn-primary"
dialogProps={
Object {
@ -358,7 +361,7 @@ exports[`admin_console/team_channel_settings/channel/ChannelMembers should match
defaultMessage="Add Members"
id="admin.team_settings.team_details.add_members"
/>
</Memo(Connect(ToggleModalButton))>
</ToggleModalButton>
}
className=""
id="channelMembers"

View File

@ -17,7 +17,7 @@ exports[`admin_console/team_channel_settings/group/GroupRow should match snapsho
<span
className="group-description row-content"
>
<Connect(ToggleModalButton)
<ToggleModalButton
className="color--link"
dialogProps={
Object {
@ -41,7 +41,7 @@ exports[`admin_console/team_channel_settings/group/GroupRow should match snapsho
}
}
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</span>
<div
className="group-description row-content roles"

View File

@ -60,6 +60,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -153,6 +154,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -246,6 +248,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -339,6 +342,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -423,6 +427,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -507,6 +512,7 @@ exports[`components/admin_console/team_channel_settings/group/UsersToRemoveRole
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -3,7 +3,7 @@
exports[`admin_console/team_channel_settings/team/TeamGroups should match snapshot 1`] = `
<AdminPanel
button={
<Memo(Connect(ToggleModalButton))
<ToggleModalButton
className="btn btn-primary"
dialogProps={
Object {
@ -61,7 +61,7 @@ exports[`admin_console/team_channel_settings/team/TeamGroups should match snapsh
defaultMessage="Add Group"
id="admin.team_settings.team_details.add_group"
/>
</Memo(Connect(ToggleModalButton))>
</ToggleModalButton>
}
className=""
id="team_groups"

View File

@ -3,7 +3,7 @@
exports[`admin_console/team_channel_settings/team/TeamMembers should match snapshot 1`] = `
<AdminPanel
button={
<Memo(Connect(ToggleModalButton))
<ToggleModalButton
className="btn btn-primary"
dialogProps={
Object {
@ -46,7 +46,7 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps
defaultMessage="Add Members"
id="admin.team_settings.team_details.add_members"
/>
</Memo(Connect(ToggleModalButton))>
</ToggleModalButton>
}
className=""
id="teamMembers"
@ -187,6 +187,7 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -225,6 +226,7 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -263,6 +265,7 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -286,7 +289,7 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps
exports[`admin_console/team_channel_settings/team/TeamMembers should match snapshot loading no users 1`] = `
<AdminPanel
button={
<Memo(Connect(ToggleModalButton))
<ToggleModalButton
className="btn btn-primary"
dialogProps={
Object {
@ -329,7 +332,7 @@ exports[`admin_console/team_channel_settings/team/TeamMembers should match snaps
defaultMessage="Add Members"
id="admin.team_settings.team_details.add_members"
/>
</Memo(Connect(ToggleModalButton))>
</ToggleModalButton>
}
className=""
id="teamMembers"

View File

@ -84,6 +84,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -128,6 +129,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -187,6 +189,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -234,6 +237,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -278,6 +282,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -337,6 +342,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -453,6 +459,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -505,6 +512,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -556,6 +564,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -603,6 +612,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -647,6 +657,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -706,6 +717,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -753,6 +765,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -797,6 +810,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -856,6 +870,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -972,6 +987,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1016,6 +1032,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1075,6 +1092,7 @@ exports[`components/admin_console/user_grid/UserGrid should match snapshot with
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -137,6 +137,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -213,6 +214,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -757,6 +759,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with no plugin i
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -966,6 +969,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1042,6 +1046,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1586,6 +1591,7 @@ exports[`components/ChannelHeaderDropdown should match snapshot with plugins 1`]
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/channel_notifications_modal/CollapseView should match snapshot, DESKTOP on collapsed view 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Describe
globalNotifyLevel="default"
@ -21,7 +21,7 @@ exports[`components/channel_notifications_modal/CollapseView should match snapsh
`;
exports[`components/channel_notifications_modal/CollapseView should match snapshot, MARK_UNREAD on collapsed view 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Describe
globalNotifyLevel="default"
@ -41,7 +41,7 @@ exports[`components/channel_notifications_modal/CollapseView should match snapsh
`;
exports[`components/channel_notifications_modal/CollapseView should match snapshot, PUSH on collapsed view 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Describe
globalNotifyLevel="default"

View File

@ -35,8 +35,8 @@ import './feature_restricted_modal.scss';
type FeatureRestrictedModalProps = {
titleAdminPreTrial: string;
messageAdminPreTrial: string;
titleAdminPostTrial: string;
messageAdminPostTrial: string;
titleAdminPostTrial?: string;
messageAdminPostTrial?: string;
titleEndUser?: string;
messageEndUser?: string;
customSecondaryButton?: {msg: string; action: () => void};

View File

@ -48,6 +48,7 @@ exports[`components/integrations/InstalledOutgoingWebhooks should match snapshot
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -155,6 +156,7 @@ exports[`components/integrations/InstalledOutgoingWebhooks should match snapshot
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -24,7 +24,7 @@ function makeGetChannelNamesMap() {
return createSelector(
'makeGetChannelNamesMap',
getChannelNameToDisplayNameMap,
(state: GlobalState, props: OwnProps) => props && props.channelNamesMap,
(_: GlobalState, props: OwnProps) => props && props.channelNamesMap,
(channelNamesMap, channelMentions) => {
if (channelMentions) {
return Object.assign({}, channelMentions, channelNamesMap);
@ -63,6 +63,7 @@ function makeMapStateToProps() {
}
const connector = connect(makeMapStateToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;
export default connector(Markdown);

View File

@ -5,6 +5,8 @@ import React from 'react';
import type {PostImage, PostType} from '@mattermost/types/posts';
import type {HighlightWithoutNotificationKey} from 'mattermost-redux/selectors/entities/users';
import PostEditedIndicator from 'components/post_view/post_edited_indicator';
import type EmojiMap from 'utils/emoji_map';
@ -53,6 +55,7 @@ export type OwnProps = {
* An array of words that can be used to mention a user
*/
mentionKeys?: MentionKey[];
highlightKeys?: HighlightWithoutNotificationKey[];
/**
* Any extra props that should be passed into the image component
@ -92,6 +95,7 @@ function Markdown({
message = '',
channelNamesMap,
mentionKeys,
highlightKeys,
imageProps,
channelId,
hasPluginTooltips,
@ -123,6 +127,7 @@ function Markdown({
autolinkedUrlSchemes,
siteURL,
mentionKeys,
highlightKeys,
atMentions: true,
channelNamesMap,
proxyImages: hasImageProxy && proxyImages,

View File

@ -95,6 +95,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -133,6 +134,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -171,6 +173,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -209,6 +212,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -247,6 +251,7 @@ exports[`components/MoreDirectChannels should exclude deleted users if there is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -380,6 +385,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -418,6 +424,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -456,6 +463,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -499,6 +507,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -539,6 +548,7 @@ exports[`components/MoreDirectChannels should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -1,14 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {type ConnectedProps, connect} from 'react-redux';
import type {Channel} from '@mattermost/types/channels';
import type {Post} from '@mattermost/types/posts';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {
getMyGroupMentionKeysForChannel,
getMyGroupMentionKeys,
@ -16,15 +17,16 @@ import {
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
import {getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
import {getCurrentUserMentionKeys, getHighlightWithoutNotificationKeys} from 'mattermost-redux/selectors/entities/users';
import {canManageMembers} from 'utils/channel_utils';
import {Preferences} from 'utils/constants';
import {isEnterpriseOrCloudOrSKUStarterFree} from 'utils/license_utils';
import type {MentionKey} from 'utils/text_formatting';
import type {GlobalState} from 'types/store';
import PostMarkdown from './post_markdown';
import PostMarkdown, {type OwnProps} from './post_markdown';
export function makeGetMentionKeysForPost(): (
state: GlobalState,
@ -54,18 +56,19 @@ export function makeGetMentionKeysForPost(): (
);
}
type OwnProps = {
channelId: string;
mentionKeys: MentionKey[];
post?: Post;
};
function makeMapStateToProps() {
const getMentionKeysForPost = makeGetMentionKeysForPost();
return (state: GlobalState, ownProps: OwnProps) => {
const channel = getChannel(state, ownProps.channelId);
const currentTeam = getCurrentTeam(state) || {};
const license = getLicense(state);
const subscriptionProduct = getSubscriptionProduct(state);
const config = getConfig(state);
const isEnterpriseReady = config.BuildEnterpriseReady === 'true';
return {
channel,
currentTeam,
@ -73,11 +76,18 @@ function makeMapStateToProps() {
hasPluginTooltips: Boolean(state.plugins.components.LinkTooltip),
isUserCanManageMembers: channel && canManageMembers(state, channel),
mentionKeys: getMentionKeysForPost(state, ownProps.post, channel),
highlightKeys: getHighlightWithoutNotificationKeys(state),
isMilitaryTime: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
timezone: getCurrentTimezone(state),
hideGuestTags: getConfig(state).HideGuestTags === 'true',
isEnterpriseOrCloudOrSKUStarterFree: isEnterpriseOrCloudOrSKUStarterFree(license, subscriptionProduct, isEnterpriseReady),
isEnterpriseReady,
};
};
}
export default connect(makeMapStateToProps)(PostMarkdown);
const connector = connect(makeMapStateToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;
export default connector(PostMarkdown);

View File

@ -10,11 +10,14 @@ import {Posts} from 'mattermost-redux/constants';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {PluginComponent} from 'types/store/plugins';
import PostMarkdown from './post_markdown';
describe('components/PostMarkdown', () => {
const baseProps = {
imageProps: {},
imageProps: {} as Record<string, unknown>,
pluginHooks: [],
message: 'message',
post: TestHelper.getPostMock(),
mentionKeys: [{key: 'a'}, {key: 'b'}, {key: 'c'}],
@ -22,6 +25,14 @@ describe('components/PostMarkdown', () => {
channel: TestHelper.getChannelMock(),
currentTeam: TestHelper.getTeamMock(),
hideGuestTags: false,
isMilitaryTime: false,
timezone: '',
highlightKeys: [],
hasPluginTooltips: false,
isUserCanManageMembers: false,
isEnterpriseOrCloudOrSKUStarterFree: true,
isEnterpriseReady: false,
dispatch: jest.fn(),
};
const state = {entities: {
@ -224,7 +235,7 @@ describe('components/PostMarkdown', () => {
return updatedMessage + '!';
},
},
],
] as PluginComponent[],
};
renderWithContext(<PostMarkdown {...props}/>, state);
expect(screen.queryByText('world', {exact: true})).not.toBeInTheDocument();
@ -258,7 +269,7 @@ describe('components/PostMarkdown', () => {
return post.message + '!';
},
},
],
] as PluginComponent[],
};
renderWithContext(<PostMarkdown {...props}/>, state);
expect(screen.queryByText('world', {exact: true})).not.toBeInTheDocument();

View File

@ -4,65 +4,45 @@
import memoize from 'memoize-one';
import React from 'react';
import type {Channel} from '@mattermost/types/channels';
import type {Post} from '@mattermost/types/posts';
import type {Team} from '@mattermost/types/teams';
import {Posts} from 'mattermost-redux/constants';
import Markdown from 'components/markdown';
import type {MentionKey, TextFormattingOptions} from 'utils/text_formatting';
import type {TextFormattingOptions} from 'utils/text_formatting';
import {renderReminderSystemBotMessage, renderSystemMessage} from './system_message_helpers';
type Props = {
import {type PropsFromRedux} from './index';
/*
export type OwnProps = {
/**
* Any extra props that should be passed into the image component
*/
imageProps?: Record<string, any>;
imageProps?: Record<string, unknown>;
/*
/**
* The post text to be rendered
*/
message: string;
/*
/**
* The optional post for which this message is being rendered
*/
post?: Post;
/*
* The id of the channel that this post is being rendered in
*/
channelId?: string;
channel: Channel;
currentTeam: Team;
options?: TextFormattingOptions;
pluginHooks?: Array<Record<string, any>>;
/**
* Whether or not to place the LinkTooltip component inside links
*/
hasPluginTooltips?: boolean;
isUserCanManageMembers?: boolean;
mentionKeys: MentionKey[];
channelId: string;
/**
* Whether or not to render the post edited indicator
* @default true
*/
showPostEditedIndicator?: boolean;
options?: TextFormattingOptions;
};
/**
* Whether the user prefers Military time
*/
isMilitaryTime?: boolean;
timezone?: string;
hideGuestTags: boolean;
}
type Props = PropsFromRedux & OwnProps;
export default class PostMarkdown extends React.PureComponent<Props> {
static defaultProps = {
@ -82,11 +62,10 @@ export default class PostMarkdown extends React.PureComponent<Props> {
});
render() {
let {message} = this.props;
const {post, mentionKeys} = this.props;
let message = this.props.message;
if (post) {
const renderedSystemMessage = renderSystemMessage(post,
if (this.props.post) {
const renderedSystemMessage = renderSystemMessage(this.props.post,
this.props.currentTeam,
this.props.channel,
this.props.hideGuestTags,
@ -98,39 +77,45 @@ export default class PostMarkdown extends React.PureComponent<Props> {
}
}
if (post && post.type === Posts.POST_TYPES.REMINDER) {
const renderedSystemBotMessage = renderReminderSystemBotMessage(post, this.props.currentTeam);
if (this.props.post && this.props.post.type === Posts.POST_TYPES.REMINDER) {
const renderedSystemBotMessage = renderReminderSystemBotMessage(this.props.post, this.props.currentTeam);
return <div>{renderedSystemBotMessage}</div>;
}
// Proxy images if we have an image proxy and the server hasn't already rewritten the post's image URLs.
const proxyImages = !post || !post.message_source || post.message === post.message_source;
const channelNamesMap = post && post.props && post.props.channel_mentions;
// Proxy images if we have an image proxy and the server hasn't already rewritten the this.props.post's image URLs.
const proxyImages = !this.props.post || !this.props.post.message_source || this.props.post.message === this.props.post.message_source;
const channelNamesMap = this.props.post && this.props.post.props && this.props.post.props.channel_mentions;
this.props.pluginHooks?.forEach((o) => {
if (o && o.hook && post) {
message = o.hook(post, message);
if (o && o.hook && this.props.post) {
message = o.hook(this.props.post, message);
}
});
let mentionHighlight = this.props.options?.mentionHighlight;
if (post && post.props) {
mentionHighlight = !post.props.mentionHighlightDisabled;
if (this.props.post && this.props.post.props) {
mentionHighlight = !this.props.post.props.mentionHighlightDisabled;
}
const options = this.getOptions(
this.props.options,
post?.props?.disable_group_highlight === true,
this.props.post?.props?.disable_group_highlight === true,
mentionHighlight,
post?.edit_at,
this.props.post?.edit_at,
);
let highlightKeys;
if (!this.props.isEnterpriseOrCloudOrSKUStarterFree && this.props.isEnterpriseReady) {
highlightKeys = this.props.highlightKeys;
}
return (
<Markdown
imageProps={this.props.imageProps}
message={message}
proxyImages={proxyImages}
mentionKeys={mentionKeys}
mentionKeys={this.props.mentionKeys}
highlightKeys={highlightKeys}
options={options}
channelNamesMap={channelNamesMap}
hasPluginTooltips={this.props.hasPluginTooltips}

View File

@ -19,7 +19,6 @@ exports[`components/post_view/PostAttachment should match snapshot 1`] = `
"onImageLoaded": [Function],
}
}
mentionKeys={Array []}
message="post message"
options={Object {}}
post={
@ -57,7 +56,6 @@ exports[`components/post_view/PostAttachment should match snapshot, on Show Less
"onImageLoaded": [Function],
}
}
mentionKeys={Array []}
message="post message"
options={Object {}}
post={
@ -95,7 +93,6 @@ exports[`components/post_view/PostAttachment should match snapshot, on Show More
"onImageLoaded": [Function],
}
}
mentionKeys={Array []}
message="post message"
options={Object {}}
post={
@ -151,7 +148,6 @@ exports[`components/post_view/PostAttachment should match snapshot, on edited po
"onImageLoaded": [Function],
}
}
mentionKeys={Array []}
message="post message"
options={Object {}}
post={
@ -190,7 +186,6 @@ exports[`components/post_view/PostAttachment should match snapshot, on ephemeral
"onImageLoaded": [Function],
}
}
mentionKeys={Array []}
message="post message"
options={Object {}}
post={

View File

@ -158,7 +158,6 @@ export default class PostMessageView extends React.PureComponent<Props, State> {
options={options}
post={post}
channelId={post.channel_id}
mentionKeys={[]}
showPostEditedIndicator={this.props.showPostEditedIndicator}
/>
</div>

View File

@ -139,6 +139,7 @@ exports[`components/ProfilePopover should disable start call button when user is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -197,7 +198,7 @@ exports[`components/ProfilePopover should disable start call button when user is
}
>
<div>
<Connect(ToggleModalButton)
<ToggleModalButton
ariaLabel="Add to a Channel"
className="btn icon-btn"
dialogProps={
@ -227,6 +228,7 @@ exports[`components/ProfilePopover should disable start call button when user is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -259,7 +261,7 @@ exports[`components/ProfilePopover should disable start call button when user is
aria-label="Add User to Channel Icon"
size={18}
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</OverlayTrigger>
<OverlayTrigger
@ -324,6 +326,7 @@ exports[`components/ProfilePopover should disable start call button when user is
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -487,6 +490,7 @@ exports[`components/ProfilePopover should hide add-to-channel option if not on t
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -592,6 +596,7 @@ exports[`components/ProfilePopover should hide add-to-channel option if not on t
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -755,6 +760,7 @@ exports[`components/ProfilePopover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -813,7 +819,7 @@ exports[`components/ProfilePopover should match snapshot 1`] = `
}
>
<div>
<Connect(ToggleModalButton)
<ToggleModalButton
ariaLabel="Add to a Channel"
className="btn icon-btn"
dialogProps={
@ -843,6 +849,7 @@ exports[`components/ProfilePopover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -875,7 +882,7 @@ exports[`components/ProfilePopover should match snapshot 1`] = `
aria-label="Add User to Channel Icon"
size={18}
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</OverlayTrigger>
<Connect(ProfilePopoverCallButton)
@ -944,6 +951,7 @@ exports[`components/ProfilePopover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1122,6 +1130,7 @@ exports[`components/ProfilePopover should match snapshot for shared user 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1181,7 +1190,7 @@ exports[`components/ProfilePopover should match snapshot for shared user 1`] = `
}
>
<div>
<Connect(ToggleModalButton)
<ToggleModalButton
ariaLabel="Add to a Channel"
className="btn icon-btn"
dialogProps={
@ -1211,6 +1220,7 @@ exports[`components/ProfilePopover should match snapshot for shared user 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1244,7 +1254,7 @@ exports[`components/ProfilePopover should match snapshot for shared user 1`] = `
aria-label="Add User to Channel Icon"
size={18}
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</OverlayTrigger>
<Connect(ProfilePopoverCallButton)
@ -1313,6 +1323,7 @@ exports[`components/ProfilePopover should match snapshot for shared user 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1477,6 +1488,7 @@ exports[`components/ProfilePopover should match snapshot when calls are disabled
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1535,7 +1547,7 @@ exports[`components/ProfilePopover should match snapshot when calls are disabled
}
>
<div>
<Connect(ToggleModalButton)
<ToggleModalButton
ariaLabel="Add to a Channel"
className="btn icon-btn"
dialogProps={
@ -1565,6 +1577,7 @@ exports[`components/ProfilePopover should match snapshot when calls are disabled
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1597,7 +1610,7 @@ exports[`components/ProfilePopover should match snapshot when calls are disabled
aria-label="Add User to Channel Icon"
size={18}
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</OverlayTrigger>
</div>
@ -1632,6 +1645,7 @@ exports[`components/ProfilePopover should match snapshot when calls are disabled
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1802,6 +1816,7 @@ exports[`components/ProfilePopover should match snapshot with custom status 1`]
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1898,7 +1913,7 @@ exports[`components/ProfilePopover should match snapshot with custom status 1`]
}
>
<div>
<Connect(ToggleModalButton)
<ToggleModalButton
ariaLabel="Add to a Channel"
className="btn icon-btn"
dialogProps={
@ -1928,6 +1943,7 @@ exports[`components/ProfilePopover should match snapshot with custom status 1`]
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -1960,7 +1976,7 @@ exports[`components/ProfilePopover should match snapshot with custom status 1`]
aria-label="Add User to Channel Icon"
size={18}
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</OverlayTrigger>
<Connect(ProfilePopoverCallButton)
@ -2029,6 +2045,7 @@ exports[`components/ProfilePopover should match snapshot with custom status 1`]
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -2199,6 +2216,7 @@ exports[`components/ProfilePopover should match snapshot with custom status expi
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -2257,7 +2275,7 @@ exports[`components/ProfilePopover should match snapshot with custom status expi
}
>
<div>
<Connect(ToggleModalButton)
<ToggleModalButton
ariaLabel="Add to a Channel"
className="btn icon-btn"
dialogProps={
@ -2287,6 +2305,7 @@ exports[`components/ProfilePopover should match snapshot with custom status expi
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -2319,7 +2338,7 @@ exports[`components/ProfilePopover should match snapshot with custom status expi
aria-label="Add User to Channel Icon"
size={18}
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</OverlayTrigger>
<Connect(ProfilePopoverCallButton)
@ -2388,6 +2407,7 @@ exports[`components/ProfilePopover should match snapshot with custom status expi
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -2551,6 +2571,7 @@ exports[`components/ProfilePopover should match snapshot with custom status not
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -2676,6 +2697,7 @@ exports[`components/ProfilePopover should match snapshot with custom status not
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -2839,6 +2861,7 @@ exports[`components/ProfilePopover should match snapshot with last active displa
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -2897,7 +2920,7 @@ exports[`components/ProfilePopover should match snapshot with last active displa
}
>
<div>
<Connect(ToggleModalButton)
<ToggleModalButton
ariaLabel="Add to a Channel"
className="btn icon-btn"
dialogProps={
@ -2927,6 +2950,7 @@ exports[`components/ProfilePopover should match snapshot with last active displa
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -2959,7 +2983,7 @@ exports[`components/ProfilePopover should match snapshot with last active displa
aria-label="Add User to Channel Icon"
size={18}
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</OverlayTrigger>
<Connect(ProfilePopoverCallButton)
@ -3028,6 +3052,7 @@ exports[`components/ProfilePopover should match snapshot with last active displa
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -3166,6 +3191,7 @@ exports[`components/ProfilePopover should match snapshot with no last active dis
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -3224,7 +3250,7 @@ exports[`components/ProfilePopover should match snapshot with no last active dis
}
>
<div>
<Connect(ToggleModalButton)
<ToggleModalButton
ariaLabel="Add to a Channel"
className="btn icon-btn"
dialogProps={
@ -3254,6 +3280,7 @@ exports[`components/ProfilePopover should match snapshot with no last active dis
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -3286,7 +3313,7 @@ exports[`components/ProfilePopover should match snapshot with no last active dis
aria-label="Add User to Channel Icon"
size={18}
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</OverlayTrigger>
<Connect(ProfilePopoverCallButton)
@ -3355,6 +3382,7 @@ exports[`components/ProfilePopover should match snapshot with no last active dis
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -3468,6 +3496,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -3602,6 +3631,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -3946,6 +3976,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -4027,6 +4058,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -4167,7 +4199,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<Connect(ToggleModalButton)
<ToggleModalButton
ariaLabel="Add to a Channel"
className="btn icon-btn"
dialogProps={
@ -4197,6 +4229,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -4225,90 +4258,27 @@ exports[`components/ProfilePopover should show the start call button when isCall
modalId="add_user_to_channel"
onClick={[Function]}
>
<ToggleModalButton
actions={
Object {
"openModal": [Function],
}
}
ariaLabel="Add to a Channel"
className="btn icon-btn"
dialogProps={
Object {
"onExited": [Function],
"user": Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": "",
"id": "user_id",
"is_bot": false,
"last_activity_at": 0,
"last_name": "",
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": "",
"notify_props": Object {
"calls_desktop_sound": "true",
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "some_username",
},
}
}
dialogType={
Object {
"$$typeof": Symbol(react.memo),
"WrappedComponent": [Function],
"compare": null,
"type": [Function],
}
}
<button
aria-label="Add to a Channel dialog"
className="style--none btn icon-btn"
id="addToChannelButton"
modalId="add_user_to_channel"
onClick={[Function]}
>
<button
aria-label="Add to a Channel dialog"
className="style--none btn icon-btn"
id="addToChannelButton"
onClick={[Function]}
<AccountPlusOutlineIcon
aria-label="Add User to Channel Icon"
size={18}
>
<AccountPlusOutlineIcon
<svg
aria-label="Add User to Channel Icon"
size={18}
fill="currentColor"
height={18}
version="1.1"
viewBox="0 0 24 24"
width={18}
xmlns="http://www.w3.org/2000/svg"
>
<svg
aria-label="Add User to Channel Icon"
fill="currentColor"
height={18}
version="1.1"
viewBox="0 0 24 24"
width={18}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.033,19C4.463,19,4,18.528,4,17.945c0-0.105,0.015-0.208,0.044-0.306c0.058-0.195,0.171-0.37,0.324-0.501c0.076-0.066,0.163-0.121,0.258-0.162l3.093-1.857C8.387,15.52,9.151,15.75,10,15.75s1.613-0.23,2.28-0.631l0.352,0.211
<path
d="M5.033,19C4.463,19,4,18.528,4,17.945c0-0.105,0.015-0.208,0.044-0.306c0.058-0.195,0.171-0.37,0.324-0.501c0.076-0.066,0.163-0.121,0.258-0.162l3.093-1.857C8.387,15.52,9.151,15.75,10,15.75s1.613-0.23,2.28-0.631l0.352,0.211
c0.302-0.606,0.701-1.156,1.181-1.624l-0.006-0.004c1.045-1.391,1.622-3.311,1.622-5.203C15.429,5.21,13.247,3,10,3
S4.571,5.21,4.571,8.5c0,1.891,0.577,3.812,1.622,5.203l-2.515,1.51C2.653,15.727,2,16.783,2,17.945C2,19.63,3.361,21,5.033,21
h7.776c-0.352-0.608-0.599-1.282-0.719-2H5.033z M10,5c1.894,0,3.429,1.084,3.429,3.5c0,1.482-0.485,3.117-1.353,4.163
@ -4316,12 +4286,11 @@ exports[`components/ProfilePopover should show the start call button when isCall
c-0.226-0.119-0.437-0.272-0.633-0.453c-0.116-0.108-0.225-0.229-0.331-0.356c-0.072-0.086-0.143-0.174-0.209-0.268
C7.55,12.164,7.403,11.91,7.272,11.64c-0.194-0.406-0.351-0.846-0.466-1.3C6.729,10.037,6.67,9.728,6.631,9.419
c-0.04-0.308-0.06-0.617-0.06-0.919C6.571,6.084,8.106,5,10,5z M17,14h2v3h3v2h-3v3h-2v-3h-3v-2h3V14"
/>
</svg>
</AccountPlusOutlineIcon>
</button>
</ToggleModalButton>
</Connect(ToggleModalButton)>
/>
</svg>
</AccountPlusOutlineIcon>
</button>
</ToggleModalButton>
</div>
</OverlayTrigger>
</OverlayTrigger>
@ -4430,6 +4399,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -4511,6 +4481,7 @@ exports[`components/ProfilePopover should show the start call button when isCall
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -5,7 +5,7 @@ import React from 'react';
import type {ReactNode, RefObject} from 'react';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
type Props = {
@ -27,36 +27,33 @@ type Props = {
/**
* The setting UI when it is maximized (open)
*/
max: ReactNode | null;
max: ReactNode;
// Props to pass through for SettingItemMin
updateSection: (section: string) => void;
title?: ReactNode;
disableOpen?: boolean;
isDisabled?: boolean;
describe?: ReactNode;
/**
* Replacement in place of edit button when the setting (in collapsed mode) is disabled
*/
collapsedEditButtonWhenDisabled?: ReactNode;
}
export default class SettingItem extends React.PureComponent<Props> {
minRef: RefObject<SettingItemMinComponent>;
static defaultProps = {
infoPosition: 'bottom',
saving: false,
section: '',
containerStyle: '',
};
constructor(props: Props) {
super(props);
this.minRef = React.createRef();
}
focusEditButton(): void {
this.minRef.current?.focus();
}
componentDidUpdate(prevProps: Props) {
if (prevProps.active && !this.props.active && this.props.areAllSectionsInactive) {
this.focusEditButton();
// We want to bring back focus to the edit button when the section is opened and then closed along with all sections are closed
if (!this.props.active && prevProps.active && this.props.areAllSectionsInactive) {
this.minRef.current?.focus();
}
}
@ -67,12 +64,13 @@ export default class SettingItem extends React.PureComponent<Props> {
return (
<SettingItemMin
ref={this.minRef}
title={this.props.title}
updateSection={this.props.updateSection}
describe={this.props.describe}
section={this.props.section}
disableOpen={this.props.disableOpen}
ref={this.minRef}
isDisabled={this.props.isDisabled}
collapsedEditButtonWhenDisabled={this.props.collapsedEditButtonWhenDisabled}
/>
);
}

View File

@ -36,26 +36,26 @@ describe('components/SettingItemMin', () => {
expect(wrapper).toMatchSnapshot();
});
test('should have called updateSection on handleUpdateSection with section', () => {
test('should have called updateSection on handleClick with section', () => {
const updateSection = jest.fn();
const props = {...baseProps, updateSection};
const wrapper = shallow<SettingItemMin>(
<SettingItemMin {...props}/>,
);
wrapper.instance().handleUpdateSection({preventDefault: jest.fn()} as any);
wrapper.instance().handleClick({preventDefault: jest.fn()} as any);
expect(updateSection).toHaveBeenCalled();
expect(updateSection).toHaveBeenCalledWith('section');
});
test('should have called updateSection on handleUpdateSection with empty string', () => {
test('should have called updateSection on handleClick with empty string', () => {
const updateSection = jest.fn();
const props = {...baseProps, updateSection, section: ''};
const wrapper = shallow<SettingItemMin>(
<SettingItemMin {...props}/>,
);
wrapper.instance().handleUpdateSection({preventDefault: jest.fn()} as any);
wrapper.instance().handleClick({preventDefault: jest.fn()} as any);
expect(updateSection).toHaveBeenCalled();
expect(updateSection).toHaveBeenCalledWith('');
});

View File

@ -0,0 +1,118 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React, {type ReactNode, type MouseEvent} from 'react';
import {FormattedMessage} from 'react-intl';
import EditIcon from 'components/widgets/icons/fa_edit_icon';
import {a11yFocus} from 'utils/utils';
interface Props {
/**
* Settings title
*/
title: ReactNode;
/**
* Option to disable opening the setting
*/
isDisabled?: boolean;
/**
* Settings or tab section
*/
section: string;
/**
* Function to update section
*/
updateSection: (section: string) => void;
/**
* Settings description
*/
describe?: ReactNode;
/**
* Replacement in place of edit button when the setting (in collapsed mode) is disabled
*/
collapsedEditButtonWhenDisabled?: ReactNode;
}
export default class SettingItemMin extends React.PureComponent<Props> {
private edit: HTMLButtonElement | null = null;
focus() {
a11yFocus(this.edit);
}
private getEdit = (node: HTMLButtonElement) => {
this.edit = node;
};
handleClick = (e: MouseEvent<HTMLDivElement | HTMLButtonElement>) => {
if (this.props.isDisabled) {
return;
}
e.preventDefault();
this.props.updateSection(this.props.section);
};
render() {
let editButtonComponent: ReactNode;
if (this.props.isDisabled) {
if (this.props.collapsedEditButtonWhenDisabled) {
editButtonComponent = this.props.collapsedEditButtonWhenDisabled;
} else {
editButtonComponent = null;
}
} else {
editButtonComponent = (
<button
ref={this.getEdit}
id={this.props.section + 'Edit'}
className='color--link style--none section-min__edit'
onClick={this.handleClick}
aria-labelledby={this.props.section + 'Title ' + this.props.section + 'Edit'}
aria-expanded={false}
>
<EditIcon/>
<FormattedMessage
id='setting_item_min.edit'
defaultMessage='Edit'
/>
</button>
);
}
return (
<div
className={classNames('section-min', {isDisabled: this.props.isDisabled})}
onClick={this.handleClick}
>
<div
className='secion-min__header'
>
<h4
id={this.props.section + 'Title'}
className={classNames('section-min__title', {isDisabled: this.props.isDisabled})}
>
{this.props.title}
</h4>
{editButtonComponent}
</div>
<div
id={this.props.section + 'Desc'}
className={classNames('section-min__describe', {isDisabled: this.props.isDisabled})}
>
{this.props.describe}
</div>
</div>
);
}
}

View File

@ -1,60 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/SettingItemMin should match snapshot 1`] = `
<div
className="section-min"
onClick={[Function]}
>
<div
className="d-flex"
>
<h4
className="section-min__title"
id="sectionTitle"
>
title
</h4>
<div
className="section-min__edit"
>
<button
aria-expanded={false}
aria-labelledby="sectionTitle sectionEdit"
className="color--link cursor--pointer style--none text-left"
id="sectionEdit"
onClick={[Function]}
>
<EditIcon />
<MemoizedFormattedMessage
defaultMessage="Edit"
id="setting_item_min.edit"
/>
</button>
</div>
</div>
<div
className="section-min__describe"
id="sectionDesc"
>
describe
</div>
</div>
`;
exports[`components/SettingItemMin should match snapshot, on disableOpen to true 1`] = `
<div
className="section-min"
onClick={[Function]}
>
<div
className="d-flex"
>
<h4
className="section-min__title"
id="sectionTitle"
>
title
</h4>
</div>
</div>
`;

View File

@ -1,18 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getIsMobileView} from 'selectors/views/browser';
import type {GlobalState} from 'types/store';
import SettingItemMin from './setting_item_min';
function mapStateToProps(state: GlobalState) {
return {
isMobileView: getIsMobileView(state),
};
}
export default connect(mapStateToProps, null, null, {forwardRef: true})(SettingItemMin);

View File

@ -1,131 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {ReactNode} from 'react';
import {FormattedMessage} from 'react-intl';
import EditIcon from 'components/widgets/icons/fa_edit_icon';
import {a11yFocus} from 'utils/utils';
interface Props {
/**
* Settings title
*/
title: ReactNode | string;
/**
* Option to disable opening the setting
*/
disableOpen?: boolean;
/**
* Settings or tab section
*/
section: string;
/**
* Function to update section
*/
updateSection: (section: string) => void;
/**
* Settings description
*/
describe?: ReactNode;
isMobileView: boolean;
}
export default class SettingItemMin extends React.PureComponent<Props> {
private edit: HTMLButtonElement | null = null;
focus(): void {
a11yFocus(this.edit);
}
private getEdit = (node: HTMLButtonElement) => {
this.edit = node;
};
handleUpdateSection = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
this.props.updateSection(this.props.section);
};
render(): JSX.Element {
let editButton = null;
let describeSection = null;
if (!this.props.disableOpen && this.props.isMobileView) {
editButton = (
<div className='section-min__edit'>
<button
id={this.props.section + 'Edit'}
className='color--link cursor--pointer style--none'
onClick={this.handleUpdateSection}
ref={this.getEdit}
aria-labelledby={this.props.section + 'Title ' + this.props.section + 'Edit'}
aria-expanded={false}
>
{this.props.describe ? this.props.describe : (
<FormattedMessage
id='setting_item_min.edit'
defaultMessage='Edit'
/>
)}
<i className='icon icon-chevron-down'/>
</button>
</div>
);
} else if (!this.props.disableOpen) {
editButton = (
<div className='section-min__edit'>
<button
id={this.props.section + 'Edit'}
className='color--link cursor--pointer style--none text-left'
onClick={this.handleUpdateSection}
ref={this.getEdit}
aria-labelledby={this.props.section + 'Title ' + this.props.section + 'Edit'}
aria-expanded={false}
>
<EditIcon/>
<FormattedMessage
id='setting_item_min.edit'
defaultMessage='Edit'
/>
</button>
</div>
);
describeSection = (
<div
id={this.props.section + 'Desc'}
className='section-min__describe'
>
{this.props.describe}
</div>
);
}
return (
<div
className='section-min'
onClick={this.handleUpdateSection}
>
<div className='d-flex'>
<h4
id={this.props.section + 'Title'}
className='section-min__title'
>
{this.props.title}
</h4>
{editButton}
</div>
{describeSection}
</div>
);
}
}

View File

@ -37,7 +37,7 @@ exports[`components/sidebar/invite_members_button should match snapshot 1`] = `
}
teamId="team_id2sss"
>
<Connect(ToggleModalButton)
<ToggleModalButton
ariaLabel="Invite Members"
className="intro-links color--link cursor--pointer"
dialogType={
@ -52,51 +52,30 @@ exports[`components/sidebar/invite_members_button should match snapshot 1`] = `
modalId="invitation"
onClick={[Function]}
>
<ToggleModalButton
actions={
Object {
"openModal": [Function],
}
}
ariaLabel="Invite Members"
className="intro-links color--link cursor--pointer"
dialogType={
Object {
"$$typeof": Symbol(react.memo),
"WrappedComponent": [Function],
"compare": null,
"type": [Function],
}
}
<button
aria-label="Invite Members dialog"
className="style--none intro-links color--link cursor--pointer"
id="inviteMembersButton"
modalId="invitation"
onClick={[Function]}
>
<button
aria-label="Invite Members dialog"
className="style--none intro-links color--link cursor--pointer"
id="inviteMembersButton"
onClick={[Function]}
<div
aria-label="Invite Members"
className="SidebarChannelNavigator__inviteMembersLhsButton"
>
<div
aria-label="Invite Members"
className="SidebarChannelNavigator__inviteMembersLhsButton"
<i
className="icon-plus-box"
/>
<FormattedMessage
defaultMessage="Invite Members"
id="sidebar_left.inviteMembers"
>
<i
className="icon-plus-box"
/>
<FormattedMessage
defaultMessage="Invite Members"
id="sidebar_left.inviteMembers"
>
<span>
Invite Members
</span>
</FormattedMessage>
</div>
</button>
</ToggleModalButton>
</Connect(ToggleModalButton)>
<span>
Invite Members
</span>
</FormattedMessage>
</div>
</button>
</ToggleModalButton>
</TeamPermissionGate>
</Connect(TeamPermissionGate)>
</InviteMembersButton>

View File

@ -102,7 +102,7 @@ exports[`components/TeamSettings/OpenInvite should match snapshot on active with
`;
exports[`components/TeamSettings/OpenInvite should match snapshot on non active allowing open invite 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe="Yes"
section="open_invite"
title="Allow any user with an account on this server to join this team"
@ -111,7 +111,7 @@ exports[`components/TeamSettings/OpenInvite should match snapshot on non active
`;
exports[`components/TeamSettings/OpenInvite should match snapshot on non active with groupConstrained 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe="No, members of this team are added and removed by linked groups."
section="open_invite"
title="Allow any user with an account on this server to join this team"
@ -120,7 +120,7 @@ exports[`components/TeamSettings/OpenInvite should match snapshot on non active
`;
exports[`components/TeamSettings/OpenInvite should match snapshot on non active without groupConstrained 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe="No"
section="open_invite"
title="Allow any user with an account on this server to join this team"

View File

@ -51,7 +51,7 @@ exports[`components/TeamSettings hide invite code if no permissions for team inv
<div
className="divider-dark first"
/>
<Connect(SettingItemMin)
<SettingItemMin
describe="name"
section="name"
title="Team Name"
@ -60,7 +60,7 @@ exports[`components/TeamSettings hide invite code if no permissions for team inv
<div
className="divider-light"
/>
<Connect(SettingItemMin)
<SettingItemMin
describe=""
section="description"
title="Team Description"
@ -92,7 +92,7 @@ exports[`components/TeamSettings hide invite code if no permissions for team inv
<div
className="divider-light"
/>
<Connect(SettingItemMin)
<SettingItemMin
describe=""
section="allowed_domains"
title="allowedDomains"
@ -112,7 +112,7 @@ exports[`components/TeamSettings hide invite code if no permissions for team inv
<div
className="divider-light"
/>
<Connect(SettingItemMin)
<SettingItemMin
describe="Click 'Edit' to regenerate Invite Code."
section="invite_id"
title="Invite Code"
@ -176,7 +176,7 @@ exports[`components/TeamSettings hide invite code if no permissions for team inv
<div
className="divider-dark first"
/>
<Connect(SettingItemMin)
<SettingItemMin
describe="name"
section="name"
title="Team Name"
@ -185,7 +185,7 @@ exports[`components/TeamSettings hide invite code if no permissions for team inv
<div
className="divider-light"
/>
<Connect(SettingItemMin)
<SettingItemMin
describe=""
section="description"
title="Team Description"
@ -217,7 +217,7 @@ exports[`components/TeamSettings hide invite code if no permissions for team inv
<div
className="divider-light"
/>
<Connect(SettingItemMin)
<SettingItemMin
describe=""
section="allowed_domains"
title="allowedDomains"
@ -295,7 +295,7 @@ exports[`components/TeamSettings should match snapshot when team is group constr
<div
className="divider-dark first"
/>
<Connect(SettingItemMin)
<SettingItemMin
describe="TestTeam"
section="name"
title="Team Name"
@ -304,7 +304,7 @@ exports[`components/TeamSettings should match snapshot when team is group constr
<div
className="divider-light"
/>
<Connect(SettingItemMin)
<SettingItemMin
describe="The Test Team"
section="description"
title="Team Description"

View File

@ -289,7 +289,6 @@ export default class Textbox extends React.PureComponent<Props> {
>
<PostMarkdown
message={this.props.value}
mentionKeys={[]}
channelId={this.props.channelId}
imageProps={{hideUtilities: true}}
/>

View File

@ -10,6 +10,11 @@ import {ModalIdentifiers} from 'utils/constants';
import ToggleModalButton from './toggle_modal_button';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux') as typeof import('react-redux'),
useDispatch: () => jest.fn(),
}));
class TestModal extends React.PureComponent {
render() {
return (
@ -33,7 +38,6 @@ describe('components/ToggleModalButton', () => {
role='menuitem'
modalId={ModalIdentifiers.DELETE_CHANNEL}
dialogType={TestModal}
actions={{openModal: () => true}}
>
<FormattedMessage
id='channel_header.delete'

View File

@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {ComponentType, MouseEvent, ReactNode} from 'react';
import React, {type ComponentType, type MouseEvent, type ReactNode} from 'react';
import {useIntl} from 'react-intl';
import {useDispatch} from 'react-redux';
import type {ModalData} from 'types/actions';
import {openModal} from 'actions/views/modals';
type Props = {
ariaLabel?: string;
@ -19,14 +19,25 @@ type Props = {
disabled?: boolean;
id?: string;
role?: string;
actions: {
openModal: <P>(modalData: ModalData<P>) => void;
};
};
const ToggleModalButton = ({ariaLabel, children, modalId, dialogType, dialogProps = {}, onClick, className = '', showUnread, disabled, id, actions, role}: Props) => {
const ToggleModalButton = ({
ariaLabel,
children,
modalId,
dialogType,
dialogProps = {},
onClick,
className = '',
showUnread,
disabled,
id,
role,
}: Props) => {
const intl = useIntl();
const dispatch = useDispatch();
const show = (e: MouseEvent<HTMLButtonElement>) => {
if (e) {
e.preventDefault();
@ -38,7 +49,7 @@ const ToggleModalButton = ({ariaLabel, children, modalId, dialogType, dialogProp
dialogType,
};
actions.openModal(modalData);
dispatch(openModal(modalData));
};
const ariaLabelElement = ariaLabel ? intl.formatMessage({

View File

@ -1,20 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
import {openModal} from 'actions/views/modals';
import ToggleModalButton from './toggle_modal_button';
function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators({
openModal,
}, dispatch),
};
}
export default connect(null, mapDispatchToProps)(ToggleModalButton);

View File

@ -400,6 +400,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -441,6 +442,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -482,6 +484,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -523,6 +526,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -564,6 +568,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -605,6 +610,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -646,6 +652,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -687,6 +694,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -728,6 +736,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -769,6 +778,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -810,6 +820,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -851,6 +862,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -892,6 +904,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -933,6 +946,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -974,6 +988,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -91,6 +91,7 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -132,6 +133,7 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -173,6 +175,7 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -214,6 +217,7 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
@ -255,6 +259,7 @@ exports[`component/user_group_popover/group_member_list should match snapshot 1`
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"highlight_keys": "",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/user_settings/advanced/JoinLeaveSection should match snapshot 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
section="joinLeave"
title={
<Memo(MemoizedFormattedMessage)

View File

@ -11,7 +11,7 @@ import {Preferences} from 'mattermost-redux/constants';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import {AdvancedSections} from 'utils/constants';
import {a11yFocus} from 'utils/utils';

View File

@ -8,7 +8,7 @@ import {Preferences} from 'mattermost-redux/constants';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import {AdvancedSections} from 'utils/constants';

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/user_settings/display/user_settings_theme/user_settings_theme.jsx should match snapshot 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Open to manage your theme"

View File

@ -10,7 +10,7 @@ import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
import ExternalLink from 'components/external_link';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import ImportThemeModal from 'components/user_settings/import_theme_modal';
import {Constants, ModalIdentifiers} from 'utils/constants';

View File

@ -150,7 +150,12 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
section=""
serverError=""
submit={[MockFunction]}
title="Desktop Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Desktop Notifications"
id="user.settings.notifications.desktop.title"
/>
}
updateSection={[Function]}
/>
`;
@ -247,13 +252,18 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
section=""
serverError=""
submit={[MockFunction]}
title="Desktop Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Desktop Notifications"
id="user.settings.notifications.desktop.title"
/>
}
updateSection={[Function]}
/>
`;
exports[`components/user_settings/notifications/DesktopNotificationSettings should match snapshot, on buildMinimizedSetting 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="For mentions and direct messages, without sound"
@ -261,13 +271,18 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
/>
}
section="desktop"
title="Desktop Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Desktop Notifications"
id="user.settings.notifications.desktop.title"
/>
}
updateSection={[Function]}
/>
`;
exports[`components/user_settings/notifications/DesktopNotificationSettings should match snapshot, on buildMinimizedSetting 2`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Off"
@ -275,7 +290,12 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
/>
}
section="desktop"
title="Desktop Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Desktop Notifications"
id="user.settings.notifications.desktop.title"
/>
}
updateSection={[Function]}
/>
`;
@ -430,7 +450,12 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
section=""
serverError=""
submit={[MockFunction]}
title="Desktop Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Desktop Notifications"
id="user.settings.notifications.desktop.title"
/>
}
updateSection={[Function]}
/>
`;
@ -638,7 +663,12 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
section=""
serverError=""
submit={[MockFunction]}
title="Desktop Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Desktop Notifications"
id="user.settings.notifications.desktop.title"
/>
}
updateSection={[Function]}
/>
`;
@ -892,7 +922,12 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
section=""
serverError=""
submit={[MockFunction]}
title="Desktop Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Desktop Notifications"
id="user.settings.notifications.desktop.title"
/>
}
updateSection={[Function]}
/>
`;
@ -1101,13 +1136,18 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
section=""
serverError=""
submit={[MockFunction]}
title="Desktop Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Desktop Notifications"
id="user.settings.notifications.desktop.title"
/>
}
updateSection={[Function]}
/>
`;
exports[`components/user_settings/notifications/DesktopNotificationSettings should match snapshot, on min setting 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="For mentions and direct messages, without sound"
@ -1115,7 +1155,12 @@ exports[`components/user_settings/notifications/DesktopNotificationSettings shou
/>
}
section="desktop"
title="Desktop Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Desktop Notifications"
id="user.settings.notifications.desktop.title"
/>
}
updateSection={[Function]}
/>
`;

View File

@ -19,21 +19,21 @@ jest.mock('utils/notification_sounds', () => {
describe('components/user_settings/notifications/DesktopNotificationSettings', () => {
const baseProps: ComponentProps<typeof DesktopNotificationSettings> = {
activity: NotificationLevels.MENTION,
sound: 'false',
updateSection: jest.fn(),
setParentState: jest.fn(),
submit: jest.fn(),
cancel: jest.fn(),
error: '',
active: true,
areAllSectionsInactive: false,
updateSection: jest.fn(),
onSubmit: jest.fn(),
onCancel: jest.fn(),
saving: false,
selectedSound: 'Bing',
error: '',
setParentState: jest.fn(),
areAllSectionsInactive: false,
isCollapsedThreadsEnabled: false,
activity: NotificationLevels.MENTION,
threads: NotificationLevels.ALL,
callsSelectedSound: 'Dynamic',
sound: 'false',
callsSound: 'false',
selectedSound: 'Bing',
callsSelectedSound: 'Dynamic',
isCallsRingingEnabled: false,
};
@ -81,8 +81,8 @@ describe('components/user_settings/notifications/DesktopNotificationSettings', (
expect(wrapper).toMatchSnapshot();
});
test('should call props.updateSection and props.cancel on handleMinUpdateSection', () => {
const props = {...baseProps, updateSection: jest.fn(), cancel: jest.fn()};
test('should call props.updateSection and props.onCancel on handleMinUpdateSection', () => {
const props = {...baseProps, updateSection: jest.fn(), onCancel: jest.fn()};
const wrapper = shallow<DesktopNotificationSettings>(
<DesktopNotificationSettings {...props}/>,
);
@ -90,14 +90,14 @@ describe('components/user_settings/notifications/DesktopNotificationSettings', (
wrapper.instance().handleMinUpdateSection('');
expect(props.updateSection).toHaveBeenCalledTimes(1);
expect(props.updateSection).toHaveBeenCalledWith('');
expect(props.cancel).toHaveBeenCalledTimes(1);
expect(props.cancel).toHaveBeenCalledWith();
expect(props.onCancel).toHaveBeenCalledTimes(1);
expect(props.onCancel).toHaveBeenCalledWith();
wrapper.instance().handleMinUpdateSection('desktop');
expect(props.updateSection).toHaveBeenCalledTimes(2);
expect(props.updateSection).toHaveBeenCalledWith('desktop');
expect(props.cancel).toHaveBeenCalledTimes(2);
expect(props.cancel).toHaveBeenCalledWith();
expect(props.onCancel).toHaveBeenCalledTimes(2);
expect(props.onCancel).toHaveBeenCalledWith();
});
test('should call props.updateSection on handleMaxUpdateSection', () => {

View File

@ -1,20 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {ChangeEvent, RefObject} from 'react';
import React, {type ChangeEvent, type RefObject, type ReactNode} from 'react';
import {FormattedMessage} from 'react-intl';
import ReactSelect from 'react-select';
import type {ValueType} from 'react-select';
import ReactSelect, {type ValueType} from 'react-select';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import {NotificationLevels} from 'utils/constants';
import {t} from 'utils/i18n';
import * as NotificationSounds from 'utils/notification_sounds';
import * as Utils from 'utils/utils';
import {a11yFocus} from 'utils/utils';
type SelectedOption = {
label: string;
@ -22,21 +19,21 @@ type SelectedOption = {
};
type Props = {
active: boolean;
updateSection: (section: string) => void;
onSubmit: () => void;
onCancel: () => void;
saving: boolean;
error: string;
setParentState: (key: string, value: string | boolean) => void;
areAllSectionsInactive: boolean;
isCollapsedThreadsEnabled: boolean;
activity: string;
threads?: string;
sound: string;
callsSound: string;
updateSection: (section: string) => void;
setParentState: (key: string, value: string | boolean) => void;
submit: () => void;
cancel: () => void;
error: string;
active: boolean;
areAllSectionsInactive: boolean;
saving: boolean;
selectedSound: string;
callsSelectedSound: string;
isCollapsedThreadsEnabled: boolean;
isCallsRingingEnabled: boolean;
};
@ -49,27 +46,29 @@ type State = {
export default class DesktopNotificationSettings extends React.PureComponent<Props, State> {
dropdownSoundRef: RefObject<ReactSelect>;
callsDropdownRef: RefObject<ReactSelect>;
minRef: RefObject<SettingItemMinComponent>;
editButtonRef: RefObject<SettingItemMinComponent>;
constructor(props: Props) {
super(props);
this.state = {
selectedOption: {value: props.selectedSound, label: props.selectedSound},
callsSelectedOption: {value: props.callsSelectedSound, label: props.callsSelectedSound},
blurDropdown: false,
};
this.dropdownSoundRef = React.createRef();
this.callsDropdownRef = React.createRef();
this.minRef = React.createRef();
this.editButtonRef = React.createRef();
}
focusEditButton(): void {
this.minRef.current?.focus();
this.editButtonRef.current?.focus();
}
handleMinUpdateSection = (section: string): void => {
this.props.updateSection(section);
this.props.cancel();
this.props.onCancel();
};
handleMaxUpdateSection = (section: string): void => this.props.updateSection(section);
@ -79,7 +78,7 @@ export default class DesktopNotificationSettings extends React.PureComponent<Pro
const value = e.currentTarget.getAttribute('data-value');
if (key && value) {
this.props.setParentState(key, value);
Utils.a11yFocus(e.currentTarget);
a11yFocus(e.currentTarget);
}
if (key === 'callsDesktopSound' && value === 'false') {
NotificationSounds.stopTryNotificationRing();
@ -435,9 +434,14 @@ export default class DesktopNotificationSettings extends React.PureComponent<Pro
return (
<SettingItemMax
title={Utils.localizeMessage('user.settings.notifications.desktop.title', 'Desktop Notifications')}
title={
<FormattedMessage
id={'user.settings.notifications.desktop.title'}
defaultMessage={'Desktop Notifications'}
/>
}
inputs={inputs}
submit={this.props.submit}
submit={this.props.onSubmit}
saving={this.props.saving}
serverError={this.props.error}
updateSection={this.handleMaxUpdateSection}
@ -446,56 +450,76 @@ export default class DesktopNotificationSettings extends React.PureComponent<Pro
};
buildMinimizedSetting = () => {
let formattedMessageProps;
const hasSoundOption = NotificationSounds.hasSoundOptions();
let collapsedDescription: ReactNode = null;
if (this.props.activity === NotificationLevels.MENTION) {
if (hasSoundOption && this.props.sound !== 'false') {
formattedMessageProps = {
id: t('user.settings.notifications.desktop.mentionsSound'),
defaultMessage: 'For mentions and direct messages, with sound',
};
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.mentionsSound'
defaultMessage='For mentions and direct messages, with sound'
/>
);
} else if (hasSoundOption && this.props.sound === 'false') {
formattedMessageProps = {
id: t('user.settings.notifications.desktop.mentionsNoSound'),
defaultMessage: 'For mentions and direct messages, without sound',
};
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.mentionsNoSound'
defaultMessage='For mentions and direct messages, without sound'
/>
);
} else {
formattedMessageProps = {
id: t('user.settings.notifications.desktop.mentionsSoundHidden'),
defaultMessage: 'For mentions and direct messages',
};
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.mentionsSoundHidden'
defaultMessage='For mentions and direct messages'
/>
);
}
} else if (this.props.activity === NotificationLevels.NONE) {
formattedMessageProps = {
id: t('user.settings.notifications.off'),
defaultMessage: 'Off',
};
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.off'
defaultMessage='Off'
/>
);
} else {
if (hasSoundOption && this.props.sound !== 'false') { //eslint-disable-line no-lonely-if
formattedMessageProps = {
id: t('user.settings.notifications.desktop.allSound'),
defaultMessage: 'For all activity, with sound',
};
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.allSound'
defaultMessage='For all activity, with sound'
/>
);
} else if (hasSoundOption && this.props.sound === 'false') {
formattedMessageProps = {
id: t('user.settings.notifications.desktop.allNoSound'),
defaultMessage: 'For all activity, without sound',
};
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.allNoSound'
defaultMessage='For all activity, without sound'
/>
);
} else {
formattedMessageProps = {
id: t('user.settings.notifications.desktop.allSoundHidden'),
defaultMessage: 'For all activity',
};
collapsedDescription = (
<FormattedMessage
id='user.settings.notifications.desktop.allSoundHidden'
defaultMessage='For all activity'
/>
);
}
}
return (
<SettingItemMin
title={Utils.localizeMessage('user.settings.notifications.desktop.title', 'Desktop Notifications')}
describe={<FormattedMessage {...formattedMessageProps}/>}
ref={this.editButtonRef}
title={
<FormattedMessage
id={'user.settings.notifications.desktop.title'}
defaultMessage={'Desktop Notifications'}
/>
}
describe={collapsedDescription}
section={'desktop'}
updateSection={this.handleMinUpdateSection}
ref={this.minRef}
/>
);
};

View File

@ -7,18 +7,19 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
"savePreferences": [MockFunction],
}
}
activeSection="email"
active={true}
areAllSectionsInactive={false}
currentUserId="current_user_id"
emailInterval={0}
enableEmail={false}
enableEmailBatching={false}
error=""
isCollapsedThreadsEnabled={false}
onCancel={[MockFunction]}
onChange={[MockFunction]}
onSubmit={[MockFunction]}
saving={false}
sendEmailNotifications={true}
serverError=""
setParentState={[MockFunction]}
threads="all"
updateSection={[MockFunction]}
@ -92,7 +93,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section=""
serverError=""
submit={[Function]}
title="Email Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
id="user.settings.notifications.emailNotifications"
/>
}
updateSection={[Function]}
>
<section
@ -102,7 +108,14 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
className="col-sm-12 section-title"
id="settingTitle"
>
Email Notifications
<FormattedMessage
defaultMessage="Email Notifications"
id="user.settings.notifications.emailNotifications"
>
<span>
Email Notifications
</span>
</FormattedMessage>
</h4>
<div
className="col-sm-9 col-sm-offset-3"
@ -266,7 +279,7 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
`;
exports[`components/user_settings/notifications/EmailNotificationSetting should match snapshot, active section != email and SendEmailNotifications !== true 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email notifications are not enabled"
@ -274,13 +287,18 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
/>
}
section="email"
title="Email Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
id="user.settings.notifications.emailNotifications"
/>
}
updateSection={[Function]}
/>
`;
exports[`components/user_settings/notifications/EmailNotificationSetting should match snapshot, active section != email and SendEmailNotifications = true 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Never"
@ -288,13 +306,18 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
/>
}
section="email"
title="Email Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
id="user.settings.notifications.emailNotifications"
/>
}
updateSection={[Function]}
/>
`;
exports[`components/user_settings/notifications/EmailNotificationSetting should match snapshot, active section != email, SendEmailNotifications = true and enableEmail = true 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Immediately"
@ -302,7 +325,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
/>
}
section="email"
title="Email Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
id="user.settings.notifications.emailNotifications"
/>
}
updateSection={[Function]}
/>
`;
@ -314,18 +342,19 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
"savePreferences": [MockFunction],
}
}
activeSection="email"
active={true}
areAllSectionsInactive={false}
currentUserId="current_user_id"
emailInterval={0}
enableEmail={false}
enableEmailBatching={true}
error=""
isCollapsedThreadsEnabled={false}
onCancel={[MockFunction]}
onChange={[MockFunction]}
onSubmit={[MockFunction]}
saving={false}
sendEmailNotifications={true}
serverError=""
setParentState={[MockFunction]}
threads="all"
updateSection={[MockFunction]}
@ -448,7 +477,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section=""
serverError=""
submit={[Function]}
title="Email Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
id="user.settings.notifications.emailNotifications"
/>
}
updateSection={[Function]}
>
<section
@ -458,7 +492,14 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
className="col-sm-12 section-title"
id="settingTitle"
>
Email Notifications
<FormattedMessage
defaultMessage="Email Notifications"
id="user.settings.notifications.emailNotifications"
>
<span>
Email Notifications
</span>
</FormattedMessage>
</h4>
<div
className="col-sm-9 col-sm-offset-3"
@ -701,7 +742,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
saving={false}
section="email"
serverError=""
title="Email Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
id="user.settings.notifications.emailNotifications"
/>
}
updateSection={[Function]}
/>
`;
@ -776,7 +822,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section=""
serverError="serverError"
submit={[Function]}
title="Email Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
id="user.settings.notifications.emailNotifications"
/>
}
updateSection={[Function]}
/>
`;
@ -889,7 +940,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section=""
serverError=""
submit={[Function]}
title="Email Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
id="user.settings.notifications.emailNotifications"
/>
}
updateSection={[Function]}
/>
`;
@ -964,7 +1020,12 @@ exports[`components/user_settings/notifications/EmailNotificationSetting should
section=""
serverError=""
submit={[Function]}
title="Email Notifications"
title={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email Notifications"
id="user.settings.notifications.emailNotifications"
/>
}
updateSection={[Function]}
/>
`;

View File

@ -12,24 +12,25 @@ import {Preferences, NotificationLevels} from 'utils/constants';
describe('components/user_settings/notifications/EmailNotificationSetting', () => {
const requiredProps: ComponentProps<typeof EmailNotificationSetting> = {
currentUserId: 'current_user_id',
activeSection: 'email',
active: true,
updateSection: jest.fn(),
enableEmail: false,
emailInterval: Preferences.INTERVAL_NEVER,
onSubmit: jest.fn(),
onCancel: jest.fn(),
onChange: jest.fn(),
serverError: '',
saving: false,
error: '',
setParentState: jest.fn(),
areAllSectionsInactive: false,
isCollapsedThreadsEnabled: false,
enableEmail: false,
onChange: jest.fn(),
threads: NotificationLevels.ALL,
currentUserId: 'current_user_id',
emailInterval: Preferences.INTERVAL_NEVER,
sendEmailNotifications: true,
enableEmailBatching: false,
actions: {
savePreferences: jest.fn(),
},
isCollapsedThreadsEnabled: false,
threads: NotificationLevels.ALL,
setParentState: jest.fn(),
};
test('should match snapshot', () => {
@ -68,7 +69,7 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
const props = {
...requiredProps,
sendEmailNotifications: false,
activeSection: '',
active: false,
};
const wrapper = shallow(<EmailNotificationSetting {...props}/>);
@ -79,7 +80,7 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
const props = {
...requiredProps,
sendEmailNotifications: true,
activeSection: '',
active: false,
};
const wrapper = shallow(<EmailNotificationSetting {...props}/>);
@ -90,7 +91,7 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
const props = {
...requiredProps,
sendEmailNotifications: true,
activeSection: '',
active: false,
enableEmail: true,
};
const wrapper = shallow(<EmailNotificationSetting {...props}/>);
@ -100,7 +101,7 @@ describe('components/user_settings/notifications/EmailNotificationSetting', () =
test('should match snapshot, on serverError', () => {
const newServerError = 'serverError';
const props = {...requiredProps, serverError: newServerError};
const props = {...requiredProps, error: newServerError};
const wrapper = shallow(<EmailNotificationSetting {...props}/>);
expect(wrapper).toMatchSnapshot();
});

View File

@ -1,8 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {RefObject} from 'react';
import React, {type RefObject} from 'react';
import {FormattedMessage} from 'react-intl';
import type {PreferenceType} from '@mattermost/types/preferences';
@ -12,37 +11,38 @@ import {getEmailInterval} from 'mattermost-redux/utils/notify_props';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import {Preferences, NotificationLevels} from 'utils/constants';
import {a11yFocus, localizeMessage} from 'utils/utils';
import {a11yFocus} from 'utils/utils';
const SECONDS_PER_MINUTE = 60;
type Props = {
currentUserId: string;
activeSection: string;
active: boolean;
updateSection: (section: string) => void;
enableEmail: boolean;
emailInterval: number;
onSubmit: () => void;
onCancel: () => void;
onChange: (enableEmail: UserNotifyProps['email']) => void;
serverError?: string;
saving?: boolean;
error?: string;
setParentState: (key: string, value: any) => void;
areAllSectionsInactive: boolean;
isCollapsedThreadsEnabled: boolean;
enableEmail: boolean;
onChange: (enableEmail: UserNotifyProps['email']) => void;
threads: string;
currentUserId: string;
emailInterval: number;
sendEmailNotifications: boolean;
enableEmailBatching: boolean;
actions: {
savePreferences: (currentUserId: string, emailIntervalPreference: PreferenceType[]) =>
Promise<{data: boolean}>;
};
isCollapsedThreadsEnabled: boolean;
threads: string;
setParentState: (key: string, value: any) => void;
};
type State = {
activeSection: string;
active: boolean;
emailInterval: number;
enableEmail: boolean;
enableEmailBatching: boolean;
@ -51,7 +51,7 @@ type State = {
};
export default class EmailNotificationSetting extends React.PureComponent<Props, State> {
minRef: RefObject<SettingItemMinComponent>;
editButtonRef: RefObject<SettingItemMinComponent>;
constructor(props: Props) {
super(props);
@ -61,11 +61,11 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
enableEmail,
enableEmailBatching,
sendEmailNotifications,
activeSection,
active,
} = props;
this.state = {
activeSection,
active,
emailInterval,
enableEmail,
enableEmailBatching,
@ -73,7 +73,7 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
newInterval: getEmailInterval(enableEmail && sendEmailNotifications, enableEmailBatching, emailInterval),
};
this.minRef = React.createRef();
this.editButtonRef = React.createRef();
}
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
@ -82,13 +82,13 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
enableEmail,
enableEmailBatching,
sendEmailNotifications,
activeSection,
active,
} = nextProps;
// If we're re-opening this section, reset to defaults from props
if (activeSection === 'email' && prevState.activeSection !== 'email') {
if (active && !prevState.active) {
return {
activeSection,
active,
emailInterval,
enableEmail,
enableEmailBatching,
@ -100,10 +100,10 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
if (sendEmailNotifications !== prevState.sendEmailNotifications ||
enableEmailBatching !== prevState.enableEmailBatching ||
emailInterval !== prevState.emailInterval ||
activeSection !== prevState.activeSection
active !== prevState.active
) {
return {
activeSection,
active,
emailInterval,
enableEmail,
enableEmailBatching,
@ -116,7 +116,7 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
}
focusEditButton(): void {
this.minRef.current?.focus();
this.editButtonRef.current?.focus();
}
handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -234,11 +234,16 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
return (
<SettingItemMin
title={localizeMessage('user.settings.notifications.emailNotifications', 'Email Notifications')}
ref={this.editButtonRef}
title={
<FormattedMessage
id={'user.settings.notifications.emailNotifications'}
defaultMessage={'Email Notifications'}
/>
}
describe={description}
section={'email'}
updateSection={this.handleUpdateSection}
ref={this.minRef}
/>
);
};
@ -247,7 +252,12 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
if (!this.props.sendEmailNotifications) {
return (
<SettingItemMax
title={localizeMessage('user.settings.notifications.emailNotifications', 'Email Notifications')}
title={
<FormattedMessage
id={'user.settings.notifications.emailNotifications'}
defaultMessage={'Email Notifications'}
/>
}
inputs={[
<div
key='oauthEmailInfo'
@ -259,7 +269,7 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
/>
</div>,
]}
serverError={this.props.serverError}
serverError={this.props.error}
section={'email'}
updateSection={this.handleUpdateSection}
/>
@ -359,7 +369,12 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
return (
<SettingItemMax
title={localizeMessage('user.settings.notifications.emailNotifications', 'Email Notifications')}
title={
<FormattedMessage
id={'user.settings.notifications.emailNotifications'}
defaultMessage={'Email Notifications'}
/>
}
inputs={[
<fieldset key='userNotificationEmailOptions'>
<legend className='form-legend'>
@ -416,23 +431,23 @@ export default class EmailNotificationSetting extends React.PureComponent<Props,
]}
submit={this.handleSubmit}
saving={this.props.saving}
serverError={this.props.serverError}
serverError={this.props.error}
updateSection={this.handleUpdateSection}
/>
);
};
componentDidUpdate(prevProps: Props) {
if (prevProps.activeSection === 'email' && this.props.activeSection === '') {
if (prevProps.active && !this.props.active && this.props.areAllSectionsInactive) {
this.focusEditButton();
}
}
render() {
if (this.props.activeSection !== 'email') {
return this.renderMinSettingView();
if (this.props.active) {
return this.renderMaxSettingView();
}
return this.renderMaxSettingView();
return this.renderMinSettingView();
}
}

View File

@ -4,11 +4,14 @@
import {connect, type ConnectedProps} from 'react-redux';
import {updateMe} from 'mattermost-redux/actions/users';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {isCallsEnabled, isCallsRingingEnabledOnServer} from 'selectors/calls';
import {isEnterpriseOrCloudOrSKUStarterFree} from 'utils/license_utils';
import type {GlobalState} from 'types/store';
import UserSettingsNotifications from './user_settings_notifications';
@ -18,11 +21,20 @@ const mapStateToProps = (state: GlobalState) => {
const sendPushNotifications = config.SendPushNotifications === 'true';
const enableAutoResponder = config.ExperimentalEnableAutomaticReplies === 'true';
const license = getLicense(state);
const subscriptionProduct = getSubscriptionProduct(state);
const isEnterpriseReady = config.BuildEnterpriseReady === 'true';
return {
sendPushNotifications,
enableAutoResponder,
isCollapsedThreadsEnabled: isCollapsedThreadsEnabled(state),
isCallsRingingEnabled: isCallsEnabled(state, '0.17.0') && isCallsRingingEnabledOnServer(state),
isEnterpriseOrCloudOrSKUStarterFree: isEnterpriseOrCloudOrSKUStarterFree(license, subscriptionProduct, isEnterpriseReady),
isEnterpriseReady,
};
};
@ -34,4 +46,4 @@ const connector = connect(mapStateToProps, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps<typeof connector>;
export default connect(mapStateToProps, mapDispatchToProps)(UserSettingsNotifications);
export default connector(UserSettingsNotifications);

View File

@ -22,6 +22,8 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
enableAutoResponder: false,
isCallsRingingEnabled: true,
intl: {} as IntlShape,
isEnterpriseOrCloudOrSKUStarterFree: false,
isEnterpriseReady: true,
};
test('should match snapshot', () => {
@ -32,6 +34,26 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot when its a starter free', () => {
const props = {...defaultProps, isEnterpriseOrCloudOrSKUStarterFree: true};
const wrapper = renderWithContext(
<UserSettingsNotifications {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot when its team edition', () => {
const props = {...defaultProps, isEnterpriseReady: false};
const wrapper = renderWithContext(
<UserSettingsNotifications {...props}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should show reply notifications section when CRT off', () => {
const props = {...defaultProps, isCollapsedThreadsEnabled: false};

View File

@ -19,8 +19,9 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
import ExternalLink from 'components/external_link';
import SettingItem from 'components/setting_item';
import SettingItemMax from 'components/setting_item_max';
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
import Constants, {NotificationLevels} from 'utils/constants';
import Constants, {NotificationLevels, MattermostFeatures, LicenseSkus} from 'utils/constants';
import {stopTryNotificationRing} from 'utils/notification_sounds';
import {a11yFocus} from 'utils/utils';
@ -65,6 +66,8 @@ type State = {
isCustomKeysWithNotificationInputChecked: boolean;
customKeysWithNotification: MultiInputValue[];
customKeysWithNotificationInputValue: string;
customKeysWithHighlight: MultiInputValue[];
customKeysWithHighlightInputValue: string;
firstNameKey: boolean;
channelKey: boolean;
autoResponderActive: boolean;
@ -145,9 +148,10 @@ function getDefaultStateFromProps(props: Props): State {
let channelKey = false;
let isCustomKeysWithNotificationInputChecked = false;
const customKeysWithNotification: MultiInputValue[] = [];
const customKeysWithHighlight: MultiInputValue[] = [];
if (props.user.notify_props) {
if (props.user.notify_props.mention_keys) {
if (props.user.notify_props?.mention_keys?.length > 0) {
const mentionKeys = props.user.notify_props.mention_keys.split(',').filter((key) => key.length > 0);
mentionKeys.forEach((mentionKey) => {
// Remove username(s) from list of keys
@ -166,6 +170,16 @@ function getDefaultStateFromProps(props: Props): State {
isCustomKeysWithNotificationInputChecked = customKeysWithNotification.length > 0;
}
if (props.user.notify_props?.highlight_keys?.length > 0) {
const highlightKeys = props.user.notify_props.highlight_keys.split(',').filter((key) => key.length > 0);
highlightKeys.forEach((highlightKey) => {
customKeysWithHighlight.push({
label: highlightKey,
value: highlightKey,
});
});
}
firstNameKey = props.user.notify_props?.first_name === 'true';
channelKey = props.user.notify_props?.channel === 'true';
}
@ -186,6 +200,8 @@ function getDefaultStateFromProps(props: Props): State {
customKeysWithNotification,
isCustomKeysWithNotificationInputChecked,
customKeysWithNotificationInputValue: '',
customKeysWithHighlight,
customKeysWithHighlightInputValue: '',
firstNameKey,
channelKey,
autoResponderActive,
@ -249,6 +265,14 @@ class NotificationsTab extends React.PureComponent<Props, State> {
}
data.mention_keys = mentionKeys.join(',');
const highlightKeys: string[] = [];
if (this.state.customKeysWithHighlight.length > 0) {
this.state.customKeysWithHighlight.forEach((key) => {
highlightKeys.push(key.value);
});
}
data.highlight_keys = highlightKeys.join(',');
this.setState({isSaving: true});
stopTryNotificationRing();
@ -394,6 +418,61 @@ class NotificationsTab extends React.PureComponent<Props, State> {
}
};
handleChangeForCustomKeysWithHightlightInput = (values: ValueType<{ value: string }>) => {
if (values && Array.isArray(values) && values.length > 0) {
const customKeysWithHighlight = values.
map((value: MultiInputValue) => {
const formattedValue = value.value.trim();
return {value: formattedValue, label: formattedValue};
}).
filter((value) => value.value.length > 0);
this.setState({customKeysWithHighlight});
} else {
this.setState({
customKeysWithHighlight: [],
});
}
};
handleChangeForCustomKeysWithHighlightInputValue = (value: string) => {
if (!value.includes(Constants.KeyCodes.COMMA[0])) {
this.setState({customKeysWithHighlightInputValue: value});
}
};
updateCustomKeysWithHighlightWithInputValue = (newValue: State['customKeysWithHighlightInputValue']) => {
const unsavedCustomKeyWithHighlight = newValue?.trim()?.replace(COMMA_REGEX, '') ?? '';
if (unsavedCustomKeyWithHighlight.length > 0) {
const customKeysWithHighlight = [
...this.state.customKeysWithHighlight,
{
value: unsavedCustomKeyWithHighlight,
label: unsavedCustomKeyWithHighlight,
},
];
this.setState({
customKeysWithHighlight,
customKeysWithHighlightInputValue: '',
});
}
};
handleBlurForCustomKeysWithHighlightInput = () => {
this.updateCustomKeysWithHighlightWithInputValue(this.state.customKeysWithHighlightInputValue);
};
handleOnKeydownForCustomKeysWithHighlightInput = (event: React.KeyboardEvent) => {
if (event.key === Constants.KeyCodes.COMMA[0] || event.key === Constants.KeyCodes.TAB[0]) {
this.updateCustomKeysWithHighlightWithInputValue(this.state.customKeysWithHighlightInputValue);
}
};
handleCloseSettingsModal = () => {
this.props.closeModal();
};
createPushNotificationSection = () => {
const active = this.props.activeSection === 'push';
const inputs = [];
@ -824,7 +903,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
expandedSection = (
<SettingItemMax
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithNotification.title', defaultMessage: 'Keywords that trigger Notifications'})}
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithNotification.title', defaultMessage: 'Keywords That Trigger Notifications'})}
inputs={inputs}
submit={this.handleSubmit}
saving={this.state.isSaving}
@ -855,7 +934,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
return (
<SettingItem
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithNotification.title', defaultMessage: 'Keywords that trigger Notifications'})}
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithNotification.title', defaultMessage: 'Keywords That Trigger Notifications'})}
section='keysWithNotification'
active={isSectionExpanded}
areAllSectionsInactive={this.props.activeSection === ''}
@ -865,6 +944,140 @@ class NotificationsTab extends React.PureComponent<Props, State> {
/>);
};
createKeywordsWithHighlightSection = () => {
const isSectionExpanded = this.props.activeSection === 'keysWithHighlight';
let expandedSection = null;
if (isSectionExpanded) {
const inputs = [(
<div
key='userNotificationHighlightOption'
className='customKeywordsWithNotificationSubsection'
>
<label htmlFor='mentionKeysWithHighlightInput'>
<FormattedMessage
id='user.settings.notifications.keywordsWithHighlight.inputTitle'
defaultMessage='Enter non case-sensitive keywords, press Tab or use commas to separate them:'
/>
</label>
<CreatableReactSelect
inputId='mentionKeysWithHighlightInput'
autoFocus={true}
isClearable={false}
isMulti={true}
styles={customKeywordsWithNotificationStyles}
className='multiInput'
placeholder=''
components={{
DropdownIndicator: () => null,
Menu: () => null,
MenuList: () => null,
}}
aria-labelledby='mentionKeysWithHighlightInput'
onChange={this.handleChangeForCustomKeysWithHightlightInput}
value={this.state.customKeysWithHighlight}
inputValue={this.state.customKeysWithHighlightInputValue}
onInputChange={this.handleChangeForCustomKeysWithHighlightInputValue}
onBlur={this.handleBlurForCustomKeysWithHighlightInput}
onKeyDown={this.handleOnKeydownForCustomKeysWithHighlightInput}
/>
</div>
)];
const extraInfo = (
<FormattedMessage
id='user.settings.notifications.keywordsWithHighlight.extraInfo'
defaultMessage='These keywords will be shown to you with a highlight when anyone sends a message that includes them.'
/>
);
expandedSection = (
<SettingItemMax
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithHighlight.title', defaultMessage: 'Keywords That Get Highlighted (Without Notifications)'})}
inputs={inputs}
submit={this.handleSubmit}
saving={this.state.isSaving}
serverError={this.state.serverError}
extraInfo={extraInfo}
updateSection={this.handleUpdateSection}
/>
);
}
let collapsedDescription = this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithHighlight.none', defaultMessage: 'None'});
if (!this.props.isEnterpriseOrCloudOrSKUStarterFree && this.props.isEnterpriseReady && this.state.customKeysWithHighlight.length > 0) {
const customKeysWithHighlightStringArray = this.state.customKeysWithHighlight.map((key) => key.value);
collapsedDescription = customKeysWithHighlightStringArray.map((key) => `"${key}"`).join(', ');
}
const collapsedEditButtonWhenDisabled = (
<RestrictedIndicator
blocked={this.props.isEnterpriseOrCloudOrSKUStarterFree && this.props.isEnterpriseReady}
feature={MattermostFeatures.HIGHLIGHT_WITHOUT_NOTIFICATION}
minimumPlanRequiredForFeature={LicenseSkus.Professional}
tooltipTitle={this.props.intl.formatMessage({
id: 'user.settings.notifications.keywordsWithHighlight.disabledTooltipTitle',
defaultMessage: 'Professional feature',
})}
tooltipMessageBlocked={this.props.intl.formatMessage({
id: 'user.settings.notifications.keywordsWithHighlight.disabledTooltipMessage',
defaultMessage:
'This feature is available on the Professional plan',
})}
titleAdminPreTrial={this.props.intl.formatMessage({
id: 'user.settings.notifications.keywordsWithHighlight.userModal.titleAdminPreTrial',
defaultMessage: 'Highlight keywords without notifications with Mattermost Professional',
})}
messageAdminPreTrial={this.props.intl.formatMessage({
id: 'user.settings.notifications.keywordsWithHighlight.userModal.messageAdminPreTrial',
defaultMessage: 'Get the ability to passively highlight keywords that you care about. Upgrade to Professional plan to unlock this feature.',
})}
titleAdminPostTrial={this.props.intl.formatMessage({
id: 'user.settings.notifications.keywordsWithHighlight.userModal.titleAdminPostTrial',
defaultMessage: 'Highlight keywords without notifications with Mattermost Professional',
})}
messageAdminPostTrial={this.props.intl.formatMessage({
id: 'user.settings.notifications.keywordsWithHighlight.userModal.messageAdminPostTrial',
defaultMessage: 'Get the ability to passively highlight keywords that you care about. Upgrade to Professional plan to unlock this feature.',
},
)}
titleEndUser={this.props.intl.formatMessage({
id: 'user.settings.notifications.keywordsWithHighlight.userModal.titleEndUser',
defaultMessage: 'Highlight keywords without notifications with Mattermost Professional',
})}
messageEndUser={this.props.intl.formatMessage(
{
id: 'user.settings.notifications.keywordsWithHighlight.userModal.messageEndUser',
defaultMessage: 'Get the ability to passively highlight keywords that you care about.{br}{br}Request your admin to upgrade to Mattermost Professional to access this feature.',
},
{
br: <br/>,
},
)}
ctaExtraContent={
<FormattedMessage
id='user.settings.notifications.keywordsWithHighlight.professional'
defaultMessage='Professional'
/>
}
clickCallback={this.handleCloseSettingsModal}
/>
);
return (
<SettingItem
title={this.props.intl.formatMessage({id: 'user.settings.notifications.keywordsWithHighlight.title', defaultMessage: 'Keywords That Get Highlighted (Without Notifications)'})}
section='keysWithHighlight'
active={isSectionExpanded}
areAllSectionsInactive={this.props.activeSection === ''}
describe={collapsedDescription}
updateSection={this.handleUpdateSection}
max={expandedSection}
isDisabled={this.props.isEnterpriseOrCloudOrSKUStarterFree && this.props.isEnterpriseReady}
collapsedEditButtonWhenDisabled={collapsedEditButtonWhenDisabled}
/>);
};
createCommentsSection = () => {
const serverError = this.state.serverError;
@ -887,7 +1100,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
<legend className='form-legend hidden-label'>
<FormattedMessage
id='user.settings.notifications.comments'
defaultMessage='Reply notifications'
defaultMessage='Reply Notifications'
/>
</legend>
<div className='radio'>
@ -951,7 +1164,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
max = (
<SettingItemMax
title={this.props.intl.formatMessage({id: 'user.settings.notifications.comments', defaultMessage: 'Reply notifications'})}
title={this.props.intl.formatMessage({id: 'user.settings.notifications.comments', defaultMessage: 'Reply Notifications'})}
extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
@ -1046,6 +1259,7 @@ class NotificationsTab extends React.PureComponent<Props, State> {
render() {
const pushNotificationSection = this.createPushNotificationSection();
const keywordsWithNotificationSection = this.createKeywordsWithNotificationSection();
const keywordsWithHighlightSection = this.createKeywordsWithHighlightSection();
const commentsSection = this.createCommentsSection();
const autoResponderSection = this.createAutoResponderSection();
@ -1110,50 +1324,68 @@ class NotificationsTab extends React.PureComponent<Props, State> {
</div>
<div className='divider-dark first'/>
<DesktopNotificationSettings
active={this.props.activeSection === 'desktop'}
updateSection={this.handleUpdateSection}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
saving={this.state.isSaving}
error={this.state.serverError}
setParentState={this.setStateValue}
areAllSectionsInactive={this.props.activeSection === ''}
isCollapsedThreadsEnabled={this.props.isCollapsedThreadsEnabled}
activity={this.state.desktopActivity}
threads={this.state.desktopThreads}
sound={this.state.desktopSound}
callsSound={this.state.callsDesktopSound}
updateSection={this.handleUpdateSection}
setParentState={this.setStateValue}
submit={this.handleSubmit}
saving={this.state.isSaving}
cancel={this.handleCancel}
error={this.state.serverError}
active={this.props.activeSection === 'desktop'}
selectedSound={this.state.desktopNotificationSound || 'default'}
callsSelectedSound={this.state.callsNotificationSound || 'default'}
isCollapsedThreadsEnabled={this.props.isCollapsedThreadsEnabled}
areAllSectionsInactive={this.props.activeSection === ''}
isCallsRingingEnabled={this.props.isCallsRingingEnabled}
/>
<div className='divider-light'/>
<EmailNotificationSetting
activeSection={this.props.activeSection}
active={this.props.activeSection === 'email'}
updateSection={this.handleUpdateSection}
enableEmail={this.state.enableEmail === 'true'}
onSubmit={this.handleSubmit}
onCancel={this.handleCancel}
onChange={this.handleEmailRadio}
saving={this.state.isSaving}
serverError={this.state.serverError}
isCollapsedThreadsEnabled={this.props.isCollapsedThreadsEnabled}
error={this.state.serverError}
setParentState={this.setStateValue}
areAllSectionsInactive={this.props.activeSection === ''}
isCollapsedThreadsEnabled={this.props.isCollapsedThreadsEnabled}
enableEmail={this.state.enableEmail === 'true'}
onChange={this.handleEmailRadio}
threads={this.state.emailThreads || ''}
/>
<div className='divider-light'/>
{pushNotificationSection}
<div className='divider-light'/>
{keywordsWithNotificationSection}
{(!this.props.isEnterpriseOrCloudOrSKUStarterFree && this.props.isEnterpriseReady) && (
<>
<div className='divider-light'/>
{keywordsWithHighlightSection}
</>
)}
<div className='divider-light'/>
{!this.props.isCollapsedThreadsEnabled && (
<>
{commentsSection}
<div className='divider-light'/>
{commentsSection}
</>
)}
{this.props.enableAutoResponder && (
autoResponderSection
<>
<div className='divider-light'/>
{autoResponderSection}
</>
)}
{/* We placed the disabled items in the last */}
{(this.props.isEnterpriseOrCloudOrSKUStarterFree && this.props.isEnterpriseReady) && (
<>
<div className='divider-light'/>
{keywordsWithHighlightSection}
</>
)}
<div className='divider-dark'/>
</div>

View File

@ -53,10 +53,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
<SettingItem
active={false}
areAllSectionsInactive={false}
containerStyle=""
infoPosition="bottom"
max={null}
saving={false}
section="password"
title={
<Memo(MemoizedFormattedMessage)
@ -97,16 +94,13 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
<SettingItem
active={false}
areAllSectionsInactive={false}
containerStyle=""
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email and Password"
id="user.settings.security.emailPwd"
/>
}
infoPosition="bottom"
max={null}
saving={false}
section="signin"
title="Sign-in Method"
updateSection={[Function]}
@ -115,7 +109,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
<br />
<Connect(ToggleModalButton)
<ToggleModalButton
className="security-links color--link"
dialogType={
Object {
@ -140,8 +134,8 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
defaultMessage="View Access History"
id="user.settings.security.viewHistory"
/>
</Connect(ToggleModalButton)>
<Connect(ToggleModalButton)
</ToggleModalButton>
<ToggleModalButton
className="security-links color--link mt-2"
dialogType={
Object {
@ -162,7 +156,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
defaultMessage="View and Log Out of Active Sessions"
id="user.settings.security.logoutActiveSessions"
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</div>
`;
@ -220,10 +214,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
<SettingItem
active={false}
areAllSectionsInactive={false}
containerStyle=""
infoPosition="bottom"
max={null}
saving={false}
section="password"
title={
<Memo(MemoizedFormattedMessage)
@ -264,16 +255,13 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
<SettingItem
active={false}
areAllSectionsInactive={false}
containerStyle=""
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email and Password"
id="user.settings.security.emailPwd"
/>
}
infoPosition="bottom"
max={null}
saving={false}
section="signin"
title="Sign-in Method"
updateSection={[Function]}
@ -282,7 +270,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
<br />
<Connect(ToggleModalButton)
<ToggleModalButton
className="security-links color--link"
dialogType={
Object {
@ -307,8 +295,8 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
defaultMessage="View Access History"
id="user.settings.security.viewHistory"
/>
</Connect(ToggleModalButton)>
<Connect(ToggleModalButton)
</ToggleModalButton>
<ToggleModalButton
className="security-links color--link mt-2"
dialogType={
Object {
@ -329,7 +317,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
defaultMessage="View and Log Out of Active Sessions"
id="user.settings.security.logoutActiveSessions"
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</div>
`;
@ -387,10 +375,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
<SettingItem
active={false}
areAllSectionsInactive={false}
containerStyle=""
infoPosition="bottom"
max={null}
saving={false}
section="password"
title={
<Memo(MemoizedFormattedMessage)
@ -431,16 +416,13 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
<SettingItem
active={false}
areAllSectionsInactive={false}
containerStyle=""
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email and Password"
id="user.settings.security.emailPwd"
/>
}
infoPosition="bottom"
max={null}
saving={false}
section="signin"
title="Sign-in Method"
updateSection={[Function]}
@ -449,7 +431,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
<br />
<Connect(ToggleModalButton)
<ToggleModalButton
className="security-links color--link"
dialogType={
Object {
@ -474,8 +456,8 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
defaultMessage="View Access History"
id="user.settings.security.viewHistory"
/>
</Connect(ToggleModalButton)>
<Connect(ToggleModalButton)
</ToggleModalButton>
<ToggleModalButton
className="security-links color--link mt-2"
dialogType={
Object {
@ -496,7 +478,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
defaultMessage="View and Log Out of Active Sessions"
id="user.settings.security.logoutActiveSessions"
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</div>
`;
@ -554,10 +536,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
<SettingItem
active={false}
areAllSectionsInactive={false}
containerStyle=""
infoPosition="bottom"
max={null}
saving={false}
section="password"
title={
<Memo(MemoizedFormattedMessage)
@ -598,16 +577,13 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
<SettingItem
active={false}
areAllSectionsInactive={false}
containerStyle=""
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Email and Password"
id="user.settings.security.emailPwd"
/>
}
infoPosition="bottom"
max={null}
saving={false}
section="signin"
title="Sign-in Method"
updateSection={[Function]}
@ -616,7 +592,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
className="divider-dark"
/>
<br />
<Connect(ToggleModalButton)
<ToggleModalButton
className="security-links color--link"
dialogType={
Object {
@ -641,8 +617,8 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
defaultMessage="View Access History"
id="user.settings.security.viewHistory"
/>
</Connect(ToggleModalButton)>
<Connect(ToggleModalButton)
</ToggleModalButton>
<ToggleModalButton
className="security-links color--link mt-2"
dialogType={
Object {
@ -663,7 +639,7 @@ exports[`components/user_settings/display/UserSettingsDisplay should match snaps
defaultMessage="View and Log Out of Active Sessions"
id="user.settings.security.logoutActiveSessions"
/>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</div>
</div>
`;

View File

@ -3,7 +3,7 @@
exports[`MfaSection rendering should render nothing when MFA is not available 1`] = `""`;
exports[`MfaSection rendering when section is collapsed and MFA is active 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Active"
@ -22,7 +22,7 @@ exports[`MfaSection rendering when section is collapsed and MFA is active 1`] =
`;
exports[`MfaSection rendering when section is collapsed and MFA is not active 1`] = `
<Connect(SettingItemMin)
<SettingItemMin
describe={
<Memo(MemoizedFormattedMessage)
defaultMessage="Inactive"

View File

@ -7,7 +7,7 @@ import {FormattedMessage} from 'react-intl';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import {getHistory} from 'utils/browser_history';

View File

@ -16,7 +16,7 @@ import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import SaveButton from 'components/save_button';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import WarningIcon from 'components/widgets/icons/fa_warning_icon';
import {Constants, DeveloperLinks} from 'utils/constants';

View File

@ -13,7 +13,7 @@ import {Preferences} from 'mattermost-redux/constants';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import {localizeMessage} from 'utils/utils';

View File

@ -11,7 +11,7 @@ import {Preferences} from 'mattermost-redux/constants';
import SettingItemMax from 'components/setting_item_max';
import SettingItemMin from 'components/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min/setting_item_min';
import type SettingItemMinComponent from 'components/setting_item_min';
import {a11yFocus} from 'utils/utils';

View File

@ -2,7 +2,7 @@
exports[`components/MenuItemToggleModalRedux should match snapshot with extra text 1`] = `
<Fragment>
<Connect(ToggleModalButton)
<ToggleModalButton
className="MenuItem__with-help"
dialogProps={
Object {
@ -22,6 +22,6 @@ exports[`components/MenuItemToggleModalRedux should match snapshot with extra te
>
Extra text
</span>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</Fragment>
`;

View File

@ -19,7 +19,7 @@ describe('components/MenuItemToggleModalRedux', () => {
expect(wrapper).toMatchInlineSnapshot(`
<Fragment>
<Connect(ToggleModalButton)
<ToggleModalButton
className=""
dialogProps={
Object {
@ -34,7 +34,7 @@ describe('components/MenuItemToggleModalRedux', () => {
>
Whatever
</span>
</Connect(ToggleModalButton)>
</ToggleModalButton>
</Fragment>
`);
});

View File

@ -9,7 +9,7 @@
padding: 0 10px;
.RestrictedIndicator__button {
padding: 0 !important;
padding: 0;
}
.RestrictedIndicator__icon-tooltip {

View File

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React, {useCallback} from 'react';
import React, {useCallback, type ReactNode} from 'react';
import {useIntl} from 'react-intl';
import type {MessageDescriptor} from 'react-intl';
@ -16,27 +16,27 @@ import {Constants, LicenseSkus, ModalIdentifiers} from 'utils/constants';
import './restricted_indicator.scss';
type RestrictedIndicatorProps = {
type Props = {
useModal?: boolean;
titleAdminPreTrial?: string;
messageAdminPreTrial?: string | React.ReactNode;
titleAdminPostTrial?: string;
messageAdminPostTrial?: string | React.ReactNode;
titleEndUser?: string;
messageEndUser?: string | React.ReactNode;
blocked?: boolean;
tooltipTitle?: string;
tooltipMessage?: string;
tooltipMessageBlocked?: string | MessageDescriptor;
ctaExtraContent?: React.ReactNode;
clickCallback?: () => void;
customSecondaryButtonInModal?: {msg: string; action: () => void};
feature?: string;
minimumPlanRequiredForFeature?: string;
tooltipTitle?: ReactNode;
tooltipMessage?: ReactNode;
tooltipMessageBlocked?: string | MessageDescriptor;
titleAdminPreTrial?: ReactNode;
messageAdminPreTrial?: ReactNode;
titleAdminPostTrial?: ReactNode;
messageAdminPostTrial?: ReactNode;
titleEndUser?: ReactNode;
messageEndUser?: ReactNode;
ctaExtraContent?: ReactNode;
clickCallback?: () => void;
customSecondaryButtonInModal?: {msg: string; action: () => void};
}
function capitalizeFirstLetter(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1);
return s?.charAt(0)?.toUpperCase() + s?.slice(1);
}
const RestrictedIndicator = ({
@ -56,7 +56,7 @@ const RestrictedIndicator = ({
customSecondaryButtonInModal,
feature,
minimumPlanRequiredForFeature,
}: RestrictedIndicatorProps) => {
}: Props) => {
const {formatMessage} = useIntl();
const getTooltipMessageBlocked = useCallback(() => {
@ -92,7 +92,10 @@ const RestrictedIndicator = ({
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='right'
overlay={(
<Tooltip className='RestrictedIndicator__icon-tooltip'>
<Tooltip
id={`${feature}-tooltip`}
className='RestrictedIndicator__icon-tooltip'
>
<span className='title'>
{tooltipTitle || formatMessage({id: 'restricted_indicator.tooltip.title', defaultMessage: '{minimumPlanRequiredForFeature} feature'}, {minimumPlanRequiredForFeature: capitalizeFirstLetter(minimumPlanRequiredForFeature!)})}
</span>
@ -107,27 +110,29 @@ const RestrictedIndicator = ({
)}
>
{useModal && blocked ? (
<ToggleModalButton
id={`${feature}-restricted-indicator`.replaceAll('.', '_')}
className='RestrictedIndicator__button'
modalId={ModalIdentifiers.FEATURE_RESTRICTED_MODAL}
dialogType={FeatureRestrictedModal}
onClick={handleClickCallback}
dialogProps={{
titleAdminPreTrial,
messageAdminPreTrial,
titleAdminPostTrial,
messageAdminPostTrial,
titleEndUser,
messageEndUser,
customSecondaryButton: customSecondaryButtonInModal,
feature,
minimumPlanRequiredForFeature,
}}
>
{icon}
{ctaExtraContent}
</ToggleModalButton>
<span>
<ToggleModalButton
id={`${feature}-restricted-indicator`?.replaceAll('.', '_')}
className='RestrictedIndicator__button'
modalId={ModalIdentifiers.FEATURE_RESTRICTED_MODAL}
dialogType={FeatureRestrictedModal}
onClick={handleClickCallback}
dialogProps={{
titleAdminPreTrial,
messageAdminPreTrial,
titleAdminPostTrial,
messageAdminPostTrial,
titleEndUser,
messageEndUser,
customSecondaryButton: customSecondaryButtonInModal,
feature,
minimumPlanRequiredForFeature,
}}
>
{icon}
{ctaExtraContent}
</ToggleModalButton>
</span>
) : (
<div className='RestrictedIndicator__content'>
{icon}

View File

@ -5428,7 +5428,7 @@
"user.settings.notifications.autoResponderHint": "Set a custom message that will be automatically sent in response to Direct Messages. Mentions in Public and Private Channels will not trigger the automated reply. Enabling Automatic Replies sets your status to Out of Office and disables email and push notifications.",
"user.settings.notifications.autoResponderPlaceholder": "Message",
"user.settings.notifications.channelWide": "Channel-wide mentions \"@channel\", \"@all\", \"@here\"",
"user.settings.notifications.comments": "Reply notifications",
"user.settings.notifications.comments": "Reply Notifications",
"user.settings.notifications.commentsAny": "Trigger notifications on messages in reply threads that I start or participate in",
"user.settings.notifications.commentsInfo": "In addition to notifications for when you're mentioned, select if you would like to receive notifications on reply threads.",
"user.settings.notifications.commentsNever": "Do not trigger notifications on messages in reply threads unless I'm mentioned",
@ -5457,8 +5457,21 @@
"user.settings.notifications.header": "Notifications",
"user.settings.notifications.icon": "Notification Settings Icon",
"user.settings.notifications.info": "Desktop notifications are available on Edge, Firefox, Safari, Chrome and Mattermost Desktop Apps.",
"user.settings.notifications.keywordsWithHighlight.disabledTooltipMessage": "This feature is available on the Professional plan",
"user.settings.notifications.keywordsWithHighlight.disabledTooltipTitle": "Professional feature",
"user.settings.notifications.keywordsWithHighlight.extraInfo": "These keywords will be shown to you with a highlight when anyone sends a message that includes them.",
"user.settings.notifications.keywordsWithHighlight.inputTitle": "Enter non case-sensitive keywords, press Tab or use commas to separate them:",
"user.settings.notifications.keywordsWithHighlight.none": "None",
"user.settings.notifications.keywordsWithHighlight.professional": "Professional",
"user.settings.notifications.keywordsWithHighlight.title": "Keywords That Get Highlighted (Without Notifications)",
"user.settings.notifications.keywordsWithHighlight.userModal.messageAdminPostTrial": "Get the ability to passively highlight keywords that you care about. Upgrade to Professional plan to unlock this feature.",
"user.settings.notifications.keywordsWithHighlight.userModal.messageAdminPreTrial": "Get the ability to passively highlight keywords that you care about. Upgrade to Professional plan to unlock this feature.",
"user.settings.notifications.keywordsWithHighlight.userModal.messageEndUser": "Get the ability to passively highlight keywords that you care about.{br}{br}Request your admin to upgrade to Mattermost Professional to access this feature.",
"user.settings.notifications.keywordsWithHighlight.userModal.titleAdminPostTrial": "Highlight keywords without notifications with Mattermost Professional",
"user.settings.notifications.keywordsWithHighlight.userModal.titleAdminPreTrial": "Highlight keywords without notifications with Mattermost Professional",
"user.settings.notifications.keywordsWithHighlight.userModal.titleEndUser": "Highlight keywords without notifications with Mattermost Professional",
"user.settings.notifications.keywordsWithNotification.extraInfo": "Notifications are triggered when someone sends a message that includes your username (\"@{username}\") or any of the options selected above.",
"user.settings.notifications.keywordsWithNotification.title": "Keywords that trigger Notifications",
"user.settings.notifications.keywordsWithNotification.title": "Keywords That Trigger Notifications",
"user.settings.notifications.learnMore": "<a>Learn more about notifications</a>",
"user.settings.notifications.never": "Never",
"user.settings.notifications.off": "Off",
@ -5637,6 +5650,7 @@
"webapp.mattermost.feature.create_multiple_teams": "Create Multiple Teams",
"webapp.mattermost.feature.custom_user_groups": "Custom User groups",
"webapp.mattermost.feature.guest_accounts": "Guest Accounts",
"webapp.mattermost.feature.highlight_without_notification": "Keywords Highlight Without Notification",
"webapp.mattermost.feature.playbooks_retro": "Playbooks Retrospective",
"webapp.mattermost.feature.start_call": "Start call",
"webapp.mattermost.feature.unlimited_file_storage": "Unlimited File Storage",

View File

@ -224,6 +224,26 @@ export const getCurrentUserMentionKeys: (state: GlobalState) => UserMentionKey[]
},
);
export type HighlightWithoutNotificationKey = {
key: string;
}
export const getHighlightWithoutNotificationKeys: (state: GlobalState) => HighlightWithoutNotificationKey[] = createSelector(
'getHighlightWithoutNotificationKeys',
getCurrentUser,
(user: UserProfile) => {
const highlightKeys: HighlightWithoutNotificationKey[] = [];
if (user?.notify_props?.highlight_keys?.length > 0) {
user.notify_props.highlight_keys.split(',').forEach((key) => {
highlightKeys.push({key});
});
}
return highlightKeys;
},
);
export const getProfileSetInCurrentChannel: (state: GlobalState) => Set<UserProfile['id']> = createSelector(
'getProfileSetInCurrentChannel',
getCurrentChannelId,

View File

@ -102,6 +102,7 @@ class TestHelper {
email: 'false',
first_name: 'false',
mark_unread: 'mention',
highlight_keys: '',
mention_keys: '',
push: 'none',
push_status: 'offline',
@ -144,6 +145,7 @@ class TestHelper {
first_name: 'false',
mark_unread: 'mention',
mention_keys: '',
highlight_keys: '',
push: 'none',
push_status: 'offline',
},
@ -472,6 +474,7 @@ class TestHelper {
first_name: 'true',
channel: 'true',
mention_keys: '',
highlight_keys: '',
...override,
};
};

View File

@ -134,7 +134,6 @@ exports[`plugins/PostMessageView should match snapshot with no extended post typ
"onImageLoaded": [Function],
}
}
mentionKeys={Array []}
message="this is some text"
options={Object {}}
post={

View File

@ -54,6 +54,13 @@ del .group-mention-link:focus {
}
}
.non-notification-highlight {
padding: 0 1px;
background-color: var(--mention-highlight-bg);
border-radius: 4px;
color: var(--mention-highlight-link);
}
.group-mention-link {
color: var(--link-color);
}

View File

@ -194,13 +194,8 @@
}
.section-min__edit {
position: relative;
top: 0;
right: 0;
text-align: left;
button {
opacity: 1;
span {
display: none;
}
}
@ -299,23 +294,6 @@
}
.settings-content {
.section-min__edit {
text-align: left;
text-decoration: none;
button {
display: flex;
width: 100%;
align-items: center;
color: rgba(var(--center-channel-color-rgb), 0.75);
text-align: left;
}
.fa {
display: inline-block;
}
}
&.minimize-settings {
display: none;
padding: 0;

View File

@ -658,9 +658,42 @@
text-decoration: underline;
}
.d-flex {
> .secion-min__header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
&.isDisabled {
cursor: default;
&:hover {
background: inherit;
}
.RestrictedIndicator__icon-tooltip-container {
flex: unset;
align-self: flex-start;
padding: 0;
button {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 6px;
background-color: rgba(var(--button-bg-rgb), 0.08);
border-radius: 12px;
color: var(--button-bg);
font-size: 12px;
font-weight: 600;
i {
font-size: 12px;
}
}
}
}
}
.section-min__title {
@ -669,18 +702,15 @@
font-size: 14px;
font-weight: 600;
line-height: 20px;
&.isDisabled {
color: rgba(var(--center-channel-color-rgb), 0.64);
}
}
.section-min__edit {
margin-bottom: 5px;
text-align: right;
.fa {
display: none;
margin-right: 5px;
font-size: 12px;
opacity: 0.5;
}
}
.section-min__describe {
@ -689,4 +719,8 @@
opacity: 0.75;
text-overflow: ellipsis;
white-space: pre;
&.isDisabled {
color: rgba(var(--center-channel-color-rgb), 0.4);
}
}

View File

@ -98,6 +98,7 @@ export type PluginComponent = {
filter?: (id: string) => boolean;
action?: (...args: any) => void; // TODO Add more concrete types?
shouldRender?: (state: GlobalState) => boolean;
hook?: (post: Post, message?: string) => string;
};
export type AppBarComponent = PluginComponent & {

View File

@ -507,6 +507,7 @@ export const MattermostFeatures = {
ALL_ENTERPRISE_FEATURES: 'mattermost.feature.all_enterprise',
UPGRADE_DOWNGRADED_WORKSPACE: 'mattermost.feature.upgrade_downgraded_workspace',
PLUGIN_FEATURE: 'mattermost.feature.plugin',
HIGHLIGHT_WITHOUT_NOTIFICATION: 'mattermost.feature.highlight_without_notification',
};
export enum LicenseSkus {

View File

@ -3,9 +3,10 @@
import moment from 'moment';
import type {Product} from '@mattermost/types/cloud';
import type {ClientLicense} from '@mattermost/types/config';
import {LicenseSkus} from 'utils/constants';
import {CloudProducts, LicenseSkus, SelfHostedProducts} from 'utils/constants';
const LICENSE_EXPIRY_NOTIFICATION = 1000 * 60 * 60 * 24 * 60; // 60 days
const LICENSE_GRACE_PERIOD = 1000 * 60 * 60 * 24 * 10; // 10 days
@ -103,3 +104,14 @@ export const licenseSKUWithFirstLetterCapitalized = (license: ClientLicense) =>
const sku = license.SkuShortName;
return sku.charAt(0).toUpperCase() + sku.slice(1);
};
export function isEnterpriseOrCloudOrSKUStarterFree(license: ClientLicense, subscriptionProduct: Product | undefined, isEnterpriseReady: boolean) {
const isCloud = license?.Cloud === 'true';
const isCloudStarterFree = isCloud && subscriptionProduct?.sku === CloudProducts.STARTER;
const isSelfHostedStarter = isEnterpriseReady && (license.IsLicensed === 'false');
const isStarterSKULicense = license.IsLicensed === 'true' && license.SelfHostedProducts === SelfHostedProducts.STARTER;
return isCloudStarterFree || isSelfHostedStarter || isStarterSKULicense;
}

View File

@ -26,6 +26,8 @@ export function mapFeatureIdToTranslation(id: string, formatMessage: Function):
return formatMessage({id: 'webapp.mattermost.feature.all_enterprise', defaultMessage: 'All Enterprise features'});
case MattermostFeatures.UPGRADE_DOWNGRADED_WORKSPACE:
return formatMessage({id: 'webapp.mattermost.feature.upgrade_downgraded_workspace', defaultMessage: 'Revert the workspace to a paid plan'});
case MattermostFeatures.HIGHLIGHT_WITHOUT_NOTIFICATION:
return formatMessage({id: 'webapp.mattermost.feature.highlight_without_notification', defaultMessage: 'Keywords Highlight Without Notification'});
default:
return '';
}

View File

@ -35,7 +35,9 @@ describe('Utils.Route', () => {
comments: 'never',
first_name: 'true',
channel: 'true',
mention_keys: ''},
mention_keys: '',
highlight_keys: '',
},
last_password_update: 0,
last_picture_update: 0,
locale: '',
@ -83,7 +85,8 @@ describe('Utils.Route', () => {
position: '',
roles: '',
props: {userid: '121'},
notify_props: {desktop: 'default',
notify_props: {
desktop: 'default',
desktop_sound: 'false',
calls_desktop_sound: 'true',
email: 'true',
@ -93,7 +96,9 @@ describe('Utils.Route', () => {
comments: 'never',
first_name: 'true',
channel: 'true',
mention_keys: ''},
mention_keys: '',
highlight_keys: '',
},
last_password_update: 0,
last_picture_update: 0,
locale: '',

View File

@ -68,6 +68,7 @@ export class TestHelper {
first_name: 'false',
mark_unread: 'mention',
mention_keys: '',
highlight_keys: '',
push: 'none',
push_status: 'offline',
},

View File

@ -14,7 +14,9 @@ import {
highlightSearchTerms,
handleUnicodeEmoji,
highlightCurrentMentions,
parseSearchTerms, autolinkChannelMentions,
highlightWithoutNotificationKeywords,
parseSearchTerms,
autolinkChannelMentions,
} from 'utils/text_formatting';
import type {ChannelNamesMap} from 'utils/text_formatting';
@ -304,6 +306,101 @@ describe('highlightCurrentMentions', () => {
});
});
describe('highlightWithoutNotificationKeywords', () => {
test('should replace highlight keywords with tokens', () => {
const text = 'This is a test message with some keywords';
const tokens = new Map();
const highlightKeys = [
{key: 'test message'},
{key: 'keywords'},
];
const expectedOutput = 'This is a $MM_HIGHLIGHTKEYWORD0$ with some $MM_HIGHLIGHTKEYWORD1$';
const expectedTokens = new Map([
['$MM_HIGHLIGHTKEYWORD0$', {
value: '<span class="non-notification-highlight">test message</span>',
originalText: 'test message',
}],
['$MM_HIGHLIGHTKEYWORD1$', {
value: '<span class="non-notification-highlight">keywords</span>',
originalText: 'keywords',
}],
]);
const output = highlightWithoutNotificationKeywords(text, tokens, highlightKeys);
expect(output).toBe(expectedOutput);
expect(tokens).toEqual(expectedTokens);
});
test('should handle empty highlightKeys array', () => {
const text = 'This is a test message';
const tokens = new Map();
const highlightKeys = [] as Array<{key: string}>;
const expectedOutput = 'This is a test message';
const expectedTokens = new Map();
const output = highlightWithoutNotificationKeywords(text, tokens, highlightKeys);
expect(output).toBe(expectedOutput);
expect(tokens).toEqual(expectedTokens);
});
test('should handle empty text', () => {
const text = '';
const tokens = new Map();
const highlightKeys = [
{key: 'test'},
{key: 'keywords'},
];
const expectedOutput = '';
const expectedTokens = new Map();
const output = highlightWithoutNotificationKeywords(text, tokens, highlightKeys);
expect(output).toBe(expectedOutput);
expect(tokens).toEqual(expectedTokens);
});
test('should handle Chinese, Korean, Russian, and Japanese words', () => {
const text = 'This is a test message with some keywords: привет, こんにちは, 안녕하세요, 你好';
const tokens = new Map();
const highlightKeys = [
{key: 'こんにちは'}, // Japanese hello
{key: '안녕하세요'}, // Korean hello
{key: 'привет'}, // Russian hello
{key: '你好'}, // Chinese hello
];
const expectedOutput = 'This is a test message with some keywords: $MM_HIGHLIGHTKEYWORD0$, $MM_HIGHLIGHTKEYWORD1$, $MM_HIGHLIGHTKEYWORD2$, $MM_HIGHLIGHTKEYWORD3$';
const expectedTokens = new Map([
['$MM_HIGHLIGHTKEYWORD0$', {
value: '<span class="non-notification-highlight">привет</span>',
originalText: 'привет',
}],
['$MM_HIGHLIGHTKEYWORD1$', {
value: '<span class="non-notification-highlight">こんにちは</span>',
originalText: 'こんにちは',
}],
['$MM_HIGHLIGHTKEYWORD2$', {
value: '<span class="non-notification-highlight">안녕하세요</span>',
originalText: '안녕하세요',
}],
['$MM_HIGHLIGHTKEYWORD3$', {
value: '<span class="non-notification-highlight">你好</span>',
originalText: '你好',
}],
]);
const output = highlightWithoutNotificationKeywords(text, tokens, highlightKeys);
expect(output).toBe(expectedOutput);
expect(tokens).toEqual(expectedTokens);
});
});
describe('parseSearchTerms', () => {
const tests = [
{

View File

@ -6,6 +6,8 @@ import type {Renderer} from 'marked';
import type {SystemEmoji} from '@mattermost/types/emojis';
import type {HighlightWithoutNotificationKey} from 'mattermost-redux/selectors/entities/users';
import {formatWithRenderer} from 'utils/markdown';
import Constants from './constants';
@ -88,6 +90,11 @@ interface TextFormattingOptionsBase {
*/
mentionKeys: MentionKey[];
/**
* A list of highlight keys for the current user to highlight without notification.
*/
highlightKeys: HighlightWithoutNotificationKey[];
/**
* Specifies whether or not to remove newlines.
*
@ -360,6 +367,10 @@ export function doFormatText(text: string, options: TextFormattingOptions, emoji
output = highlightCurrentMentions(output, tokens, options.mentionKeys);
}
if (options.highlightKeys && options.highlightKeys.length > 0) {
output = highlightWithoutNotificationKeywords(output, tokens, options.highlightKeys);
}
if (!('emoticons' in options) || options.emoticons) {
output = handleUnicodeEmoji(output, emojiMap, UNICODE_EMOJI_REGEX);
}
@ -704,6 +715,79 @@ export function highlightCurrentMentions(
return output;
}
export function highlightWithoutNotificationKeywords(
text: string,
tokens: Tokens,
highlightKeys: HighlightWithoutNotificationKey[] = [],
) {
let output = text;
// Store the new tokens in a separate map since we can't add objects to a map during iteration
const newTokens = new Map();
// Look for highlighting keywords in the tokens
tokens.forEach((token, alias) => {
const tokenOriginalText = token.originalText.toLowerCase();
if (highlightKeys.findIndex((highlightKey) => highlightKey.key.toLowerCase() === tokenOriginalText) !== -1) {
const newIndex = tokens.size + newTokens.size;
const newAlias = `$MM_HIGHLIGHTKEYWORD${newIndex}$`;
newTokens.set(newAlias, {
value: `<span class="non-notification-highlight">${alias}</span>`,
originalText: token.originalText,
});
output = output.replace(alias, newAlias);
}
});
// Copy the new tokens to the tokens map
newTokens.forEach((newToken, newAlias) => {
tokens.set(newAlias, newToken);
});
// Look for highlighting keywords in the text
function replaceHighlightKeywordsWithToken(
_: string,
prefix: string,
highlightKey: string,
suffix = '',
) {
const index = tokens.size;
const alias = `$MM_HIGHLIGHTKEYWORD${index}$`;
// Set the token map with the replacement value so that it can be replaced back later
tokens.set(alias, {
value: `<span class="non-notification-highlight">${highlightKey}</span>`,
originalText: highlightKey,
});
return prefix + alias + suffix;
}
highlightKeys.
sort((a, b) => b.key.length - a.key.length).
forEach(({key}) => {
if (!key) {
return;
}
let pattern;
if (cjkrPattern.test(key)) {
// If the key contains Chinese, Japanese, Korean or Russian characters, don't mark word boundaries
pattern = new RegExp(`()(${escapeRegex(key)})()`, 'gi');
} else {
// If the key contains only English characters, mark word boundaries
pattern = new RegExp(`(^|\\W)(${escapeRegex(key)})(\\b|_+\\b)`, 'gi');
}
// Replace the key with the token for each occurrence of the key
output = output.replace(pattern, replaceHighlightKeywordsWithToken);
});
return output;
}
const hashtagRegex = /(^|\W)(#\p{L}[\p{L}\d\-_.]*[\p{L}\d])/gu;
function autolinkHashtags(

Some files were not shown because too many files have changed in this diff Show More