Report a problem (#30444)
* Add report a problem type and allow logs config * Improve device type logic * Add tests and minor fixes * Add texts * Fix tests by avoiding circular dependencies * Fix test * Fix useexternallink updating mailtos, and changing the content of query parameters * Fix texts * Fix e2e test * Fix tsc --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
29f7c895b8
Коммит
71334c6d8b
@@ -1573,6 +1573,12 @@ components:
|
||||
type: string
|
||||
ReportAProblemLink:
|
||||
type: string
|
||||
ReportAProblemType:
|
||||
type: string
|
||||
ReportAProblemMail:
|
||||
type: string
|
||||
AllowDownloadLogs:
|
||||
type: boolean
|
||||
SupportEmail:
|
||||
type: string
|
||||
GitLabSettings:
|
||||
@@ -2047,6 +2053,12 @@ components:
|
||||
type: boolean
|
||||
ReportAProblemLink:
|
||||
type: boolean
|
||||
ReportAProblemType:
|
||||
type: boolean
|
||||
ReportAProblemMail:
|
||||
type: boolean
|
||||
AllowDownloadLogs:
|
||||
type: boolean
|
||||
SupportEmail:
|
||||
type: boolean
|
||||
GitLabSettings:
|
||||
|
||||
@@ -42,6 +42,7 @@ describe('SupportSettings', () => {
|
||||
cy.findByTestId('SupportSettings.PrivacyPolicyLinkinput').clear().type(privacyLink);
|
||||
cy.findByTestId('SupportSettings.AboutLinkinput').clear().type(aboutLink);
|
||||
cy.findByTestId('SupportSettings.HelpLinkinput').clear().type(helpLink);
|
||||
cy.findByTestId('SupportSettings.ReportAProblemTypedropdown').select('Custom link');
|
||||
cy.findByTestId('SupportSettings.ReportAProblemLinkinput').clear().type(problemLink);
|
||||
|
||||
// # Save setting then back to team view
|
||||
@@ -114,7 +115,7 @@ describe('SupportSettings', () => {
|
||||
it('MM-T1036 - Customization: Blank Help and Report a Problem hides options from help menu', () => {
|
||||
// # Change help and report links to blanks
|
||||
cy.findByTestId('SupportSettings.HelpLinkinput').type('any').clear();
|
||||
cy.findByTestId('SupportSettings.ReportAProblemLinkinput').type('any').clear();
|
||||
cy.findByTestId('SupportSettings.ReportAProblemTypedropdown').select('Hide link');
|
||||
|
||||
// # Save setting and back to team view
|
||||
saveSetting();
|
||||
@@ -157,11 +158,10 @@ describe('SupportSettings', () => {
|
||||
|
||||
// * Verify that hover shows "Help" text
|
||||
cy.uiGetHelpButton().
|
||||
trigger('mouseover', {force: true}).
|
||||
should('have.attr', 'aria-describedby').
|
||||
and('equal', 'userGuideHelpTooltip');
|
||||
trigger('mouseenter').
|
||||
should('have.attr', 'aria-describedby');
|
||||
cy.uiGetHelpButton().
|
||||
trigger('mouseout', {force: true}).
|
||||
trigger('mouseleave').
|
||||
should('not.have.attr', 'aria-describedby');
|
||||
|
||||
// # Open help menu
|
||||
@@ -184,6 +184,7 @@ describe('SupportSettings', () => {
|
||||
|
||||
// Edit help link and report a problem link
|
||||
cy.findByTestId('SupportSettings.HelpLinkinput').clear().type(helpLink);
|
||||
cy.findByTestId('SupportSettings.ReportAProblemTypedropdown').select('Custom link');
|
||||
cy.findByTestId('SupportSettings.ReportAProblemLinkinput').clear().type(problemLink);
|
||||
|
||||
// # Save setting and back to team view
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
"AboutLink": "https://mattermost.com/pl/about-mattermost",
|
||||
"HelpLink": "https://mattermost.com/pl/help/",
|
||||
"ReportAProblemLink": "https://mattermost.com/pl/report-a-bug",
|
||||
"ReportAProblemType": "link",
|
||||
"ForgotPasswordLink": "",
|
||||
"SupportEmail": "",
|
||||
"CustomTermsOfServiceEnabled": false,
|
||||
|
||||
@@ -298,6 +298,7 @@
|
||||
"AboutLink": "https://mattermost.com/pl/about-mattermost",
|
||||
"HelpLink": "https://mattermost.com/pl/help/",
|
||||
"ReportAProblemLink": "https://mattermost.com/pl/report-a-bug",
|
||||
"ReportAProblemType": "link",
|
||||
"ForgotPasswordLink": "",
|
||||
"SupportEmail": "",
|
||||
"CustomTermsOfServiceEnabled": false,
|
||||
|
||||
@@ -396,6 +396,9 @@ const defaultServerConfig: AdminConfig = {
|
||||
AboutLink: 'https://mattermost.com/pl/about-mattermost',
|
||||
HelpLink: 'https://mattermost.com/pl/help/',
|
||||
ReportAProblemLink: 'https://mattermost.com/pl/report-a-bug',
|
||||
ReportAProblemType: 'link',
|
||||
ReportAProblemMail: '',
|
||||
AllowDownloadLogs: true,
|
||||
ForgotPasswordLink: '',
|
||||
SupportEmail: '',
|
||||
CustomTermsOfServiceEnabled: false,
|
||||
|
||||
@@ -294,7 +294,10 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m
|
||||
props["PrivacyPolicyLink"] = *c.SupportSettings.PrivacyPolicyLink
|
||||
props["AboutLink"] = *c.SupportSettings.AboutLink
|
||||
props["HelpLink"] = *c.SupportSettings.HelpLink
|
||||
props["ReportAProblemType"] = *c.SupportSettings.ReportAProblemType
|
||||
props["ReportAProblemLink"] = *c.SupportSettings.ReportAProblemLink
|
||||
props["ReportAProblemMail"] = *c.SupportSettings.ReportAProblemMail
|
||||
props["AllowDownloadLogs"] = strconv.FormatBool(*c.SupportSettings.AllowDownloadLogs)
|
||||
props["ForgotPasswordLink"] = *c.SupportSettings.ForgotPasswordLink
|
||||
props["SupportEmail"] = *c.SupportSettings.SupportEmail
|
||||
props["EnableAskCommunityLink"] = strconv.FormatBool(*c.SupportSettings.EnableAskCommunityLink)
|
||||
|
||||
@@ -319,6 +319,25 @@ func TestGetClientConfig(t *testing.T) {
|
||||
"GiphySdkKey": model.ServiceSettingsDefaultGiphySdkKeyTest,
|
||||
},
|
||||
},
|
||||
{
|
||||
"report a problem values",
|
||||
&model.Config{
|
||||
SupportSettings: model.SupportSettings{
|
||||
ReportAProblemType: model.NewPointer("type"),
|
||||
ReportAProblemLink: model.NewPointer("http://example.com"),
|
||||
ReportAProblemMail: model.NewPointer("mail"),
|
||||
AllowDownloadLogs: model.NewPointer(true),
|
||||
},
|
||||
},
|
||||
"",
|
||||
nil,
|
||||
map[string]string{
|
||||
"ReportAProblemType": "type",
|
||||
"ReportAProblemLink": "http://example.com",
|
||||
"ReportAProblemMail": "mail",
|
||||
"AllowDownloadLogs": "true",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
|
||||
@@ -9308,6 +9308,22 @@
|
||||
"id": "model.config.is_valid.read_timeout.app_error",
|
||||
"translation": "Invalid value for read timeout."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.report_a_problem_link.invalid.app_error",
|
||||
"translation": "Invalid report a problem link. Must be a valid URL and start with http:// or https://."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.report_a_problem_link.missing.app_error",
|
||||
"translation": "Report a problem link is required."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.report_a_problem_mail.invalid.app_error",
|
||||
"translation": "Invalid report a problem mail. Must be a valid email address."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.report_a_problem_mail.missing.app_error",
|
||||
"translation": "Report a problem mail is required."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.restrict_direct_message.app_error",
|
||||
"translation": "Invalid direct message restriction. Must be 'any', or 'team'."
|
||||
|
||||
@@ -746,6 +746,8 @@ func (ts *TelemetryService) trackConfig() {
|
||||
"custom_terms_of_service_enabled": *cfg.SupportSettings.CustomTermsOfServiceEnabled,
|
||||
"custom_terms_of_service_re_acceptance_period": *cfg.SupportSettings.CustomTermsOfServiceReAcceptancePeriod,
|
||||
"enable_ask_community_link": *cfg.SupportSettings.EnableAskCommunityLink,
|
||||
"report_a_problem_type": *cfg.SupportSettings.ReportAProblemType,
|
||||
"allow_download_logs": *cfg.SupportSettings.AllowDownloadLogs,
|
||||
}
|
||||
|
||||
configs[TrackConfigLDAP] = map[string]any{
|
||||
|
||||
@@ -147,6 +147,12 @@ const (
|
||||
SupportSettingsDefaultSupportEmail = ""
|
||||
SupportSettingsDefaultReAcceptancePeriod = 365
|
||||
|
||||
SupportSettingsReportAProblemTypeLink = "link"
|
||||
SupportSettingsReportAProblemTypeMail = "email"
|
||||
SupportSettingsReportAProblemTypeHidden = "hidden"
|
||||
SupportSettingsReportAProblemTypeDefault = "default"
|
||||
SupportSettingsDefaultReportAProblemType = SupportSettingsReportAProblemTypeDefault
|
||||
|
||||
LdapSettingsDefaultFirstNameAttribute = ""
|
||||
LdapSettingsDefaultLastNameAttribute = ""
|
||||
LdapSettingsDefaultEmailAttribute = ""
|
||||
@@ -2146,6 +2152,9 @@ type SupportSettings struct {
|
||||
AboutLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
|
||||
HelpLink *string `access:"site_customization"`
|
||||
ReportAProblemLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
|
||||
ReportAProblemType *string `access:"site_customization,write_restrictable,cloud_restrictable"`
|
||||
ReportAProblemMail *string `access:"site_customization,write_restrictable,cloud_restrictable"`
|
||||
AllowDownloadLogs *bool `access:"site_customization,write_restrictable,cloud_restrictable"`
|
||||
ForgotPasswordLink *string `access:"site_customization,write_restrictable,cloud_restrictable"`
|
||||
SupportEmail *string `access:"site_notifications"`
|
||||
CustomTermsOfServiceEnabled *bool `access:"compliance_custom_terms_of_service"`
|
||||
@@ -2194,6 +2203,18 @@ func (s *SupportSettings) SetDefaults() {
|
||||
s.ReportAProblemLink = NewPointer(SupportSettingsDefaultReportAProblemLink)
|
||||
}
|
||||
|
||||
if s.ReportAProblemType == nil {
|
||||
s.ReportAProblemType = NewPointer(SupportSettingsDefaultReportAProblemType)
|
||||
}
|
||||
|
||||
if s.ReportAProblemMail == nil {
|
||||
s.ReportAProblemMail = NewPointer("")
|
||||
}
|
||||
|
||||
if s.AllowDownloadLogs == nil {
|
||||
s.AllowDownloadLogs = NewPointer(true)
|
||||
}
|
||||
|
||||
if !isSafeLink(s.ForgotPasswordLink) {
|
||||
*s.ForgotPasswordLink = ""
|
||||
}
|
||||
@@ -3987,6 +4008,26 @@ func (o *Config) IsValid() *AppError {
|
||||
return appErr
|
||||
}
|
||||
|
||||
if o.SupportSettings.ReportAProblemType != nil {
|
||||
if *o.SupportSettings.ReportAProblemType == SupportSettingsReportAProblemTypeMail {
|
||||
if o.SupportSettings.ReportAProblemMail == nil {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.report_a_problem_mail.missing.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
if !IsValidEmail(*o.SupportSettings.ReportAProblemMail) {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.report_a_problem_mail.invalid.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
if *o.SupportSettings.ReportAProblemType == SupportSettingsReportAProblemTypeLink {
|
||||
if o.SupportSettings.ReportAProblemLink == nil {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.report_a_problem_link.missing.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if !IsValidHTTPURL(*o.SupportSettings.ReportAProblemLink) {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.report_a_problem_link.invalid.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,52 @@ func TestConfigDefaults(t *testing.T) {
|
||||
c.SetDefaults()
|
||||
recursivelyUninitialize(&c, "config", reflect.ValueOf(&c).Elem())
|
||||
})
|
||||
t.Run("report a problem defaults", func(t *testing.T) {
|
||||
c := Config{}
|
||||
c.SetDefaults()
|
||||
require.Equal(t, SupportSettingsDefaultReportAProblemType, *c.SupportSettings.ReportAProblemType)
|
||||
require.Equal(t, SupportSettingsDefaultReportAProblemLink, *c.SupportSettings.ReportAProblemLink)
|
||||
require.Equal(t, "", *c.SupportSettings.ReportAProblemMail)
|
||||
require.Equal(t, true, *c.SupportSettings.AllowDownloadLogs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigIsValid(t *testing.T) {
|
||||
t.Run("report a problem values", func(t *testing.T) {
|
||||
t.Run("email", func(t *testing.T) {
|
||||
c := Config{}
|
||||
c.SetDefaults()
|
||||
c.SupportSettings.ReportAProblemType = NewPointer(string(SupportSettingsReportAProblemTypeMail))
|
||||
c.SupportSettings.ReportAProblemMail = nil
|
||||
require.NotNil(t, c.IsValid())
|
||||
|
||||
c.SupportSettings.ReportAProblemMail = NewPointer("")
|
||||
require.NotNil(t, c.IsValid())
|
||||
|
||||
c.SupportSettings.ReportAProblemMail = NewPointer("invalid")
|
||||
require.NotNil(t, c.IsValid())
|
||||
|
||||
c.SupportSettings.ReportAProblemMail = NewPointer("valid@email.com")
|
||||
require.Nil(t, c.IsValid())
|
||||
})
|
||||
|
||||
t.Run("link", func(t *testing.T) {
|
||||
c := Config{}
|
||||
c.SetDefaults()
|
||||
c.SupportSettings.ReportAProblemType = NewPointer(string(SupportSettingsReportAProblemTypeLink))
|
||||
c.SupportSettings.ReportAProblemLink = nil
|
||||
require.NotNil(t, c.IsValid())
|
||||
|
||||
c.SupportSettings.ReportAProblemLink = NewPointer("")
|
||||
require.NotNil(t, c.IsValid())
|
||||
|
||||
c.SupportSettings.ReportAProblemLink = NewPointer("invalid")
|
||||
require.NotNil(t, c.IsValid())
|
||||
|
||||
c.SupportSettings.ReportAProblemLink = NewPointer("http://valid.com")
|
||||
require.Nil(t, c.IsValid())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigEmptySiteName(t *testing.T) {
|
||||
|
||||
@@ -189,6 +189,7 @@
|
||||
"AboutLink": "https://mattermost.com/default-about/",
|
||||
"HelpLink": "https://mattermost.com/pl/help/",
|
||||
"ReportAProblemLink": "https://mattermost.com/pl/report-a-bug",
|
||||
"ReportAProblemType": "link",
|
||||
"ForgotPasswordLink": "",
|
||||
"SupportEmail": "feedback@mattermost.com"
|
||||
},
|
||||
|
||||
@@ -2206,13 +2206,97 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)),
|
||||
isHidden: it.configIsTrue('ExperimentalSettings', 'RestrictSystemAdmin'),
|
||||
},
|
||||
{
|
||||
type: 'dropdown',
|
||||
key: 'SupportSettings.ReportAProblemType',
|
||||
label: defineMessage({id: 'admin.support.reportAProblemTypeTitle', defaultMessage: 'Report a Problem:'}),
|
||||
help_text: defineMessage({id: 'admin.support.reportAProblemTypeDescription', defaultMessage: 'Select how the ‘Report a Problem’ option behaves. Choosing ‘Custom link’ or ‘Email address’ allows you to provide a URL or address in the next field. ‘Hide link’ removes the ‘Report a Problem’ option from the app.'}),
|
||||
options: [
|
||||
{
|
||||
display_name: defineMessage({id: 'admin.support.problemType.defaultLink', defaultMessage: 'Default link'}),
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
display_name: defineMessage({id: 'admin.support.problemType.email', defaultMessage: 'Email address'}),
|
||||
value: 'email',
|
||||
},
|
||||
{
|
||||
display_name: defineMessage({id: 'admin.support.problemType.customLink', defaultMessage: 'Custom link'}),
|
||||
value: 'link',
|
||||
},
|
||||
{
|
||||
display_name: defineMessage({id: 'admin.support.problemType.hide', defaultMessage: 'Hide link'}),
|
||||
value: 'hidden',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'defaultLicensedReportAProblemLink',
|
||||
label: defineMessage({id: 'admin.support.reportAProblemDefaultLinkTitle', defaultMessage: 'Default Report a Problem Link:'}),
|
||||
help_text: defineMessage({id: 'admin.support.reportAProblemDefaultLinkDescription', defaultMessage: 'Users will be directed to this link when they choose ‘Report a Problem’.'}),
|
||||
default: 'https://mattermost.com/pl/report_a_problem_licensed',
|
||||
isDisabled: it.all(),
|
||||
isHidden: it.any(
|
||||
it.configIsTrue('ExperimentalSettings', 'RestrictSystemAdmin'),
|
||||
it.not(it.stateMatches('SupportSettings.ReportAProblemType', /default/)),
|
||||
it.not(it.licensed),
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'defaultUnlicensedReportAProblemLink',
|
||||
label: defineMessage({id: 'admin.support.reportAProblemDefaultLinkTitle', defaultMessage: 'Default Report a Problem Link:'}),
|
||||
help_text: defineMessage({id: 'admin.support.reportAProblemDefaultLinkDescription', defaultMessage: 'Users will be directed to this link when they choose ‘Report a Problem’.'}),
|
||||
default: 'https://mattermost.com/pl/report_a_problem_unlicensed',
|
||||
isDisabled: it.all(),
|
||||
isHidden: it.any(
|
||||
it.configIsTrue('ExperimentalSettings', 'RestrictSystemAdmin'),
|
||||
it.not(it.stateMatches('SupportSettings.ReportAProblemType', /default/)),
|
||||
it.licensed,
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'SupportSettings.ReportAProblemLink',
|
||||
label: defineMessage({id: 'admin.support.problemTitle', defaultMessage: 'Report a Problem Link:'}),
|
||||
help_text: defineMessage({id: 'admin.support.problemDesc', defaultMessage: 'The URL for the Report a Problem link in the Help Menu. If this field is empty, the link is removed from the Help Menu.'}),
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)),
|
||||
isHidden: it.configIsTrue('ExperimentalSettings', 'RestrictSystemAdmin'),
|
||||
label: defineMessage({id: 'admin.support.reportAProblemLinkTitle', defaultMessage: 'Custom Report a Problem Link:'}),
|
||||
help_text: defineMessage({id: 'admin.support.reportAProblemLinkDescription', defaultMessage: 'Enter the URL that users will be directed to when they choose ‘Report a Problem’.'}),
|
||||
isDisabled: it.any(
|
||||
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)),
|
||||
),
|
||||
isHidden: it.any(
|
||||
it.configIsTrue('ExperimentalSettings', 'RestrictSystemAdmin'),
|
||||
it.not(it.stateMatches('SupportSettings.ReportAProblemType', /link/)),
|
||||
),
|
||||
validate: (value) => {
|
||||
if (!value) {
|
||||
return new ValidationResult(false, defineMessage({id: 'admin.support.reportAProblemLinkError', defaultMessage: 'Link is required'}));
|
||||
}
|
||||
return new ValidationResult(true, '');
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'SupportSettings.ReportAProblemMail',
|
||||
label: defineMessage({id: 'admin.support.reportAProblemEmailTitle', defaultMessage: 'Report a Problem Email Address:'}),
|
||||
help_text: defineMessage({id: 'admin.support.reportAProblemEmailDescription', defaultMessage: 'Enter the email address that users will be prompted to send a message to when they choose ‘Report a Problem’.'}),
|
||||
isDisabled: (it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION))),
|
||||
isHidden: it.any(
|
||||
it.configIsTrue('ExperimentalSettings', 'RestrictSystemAdmin'),
|
||||
it.not(it.stateMatches('SupportSettings.ReportAProblemType', /email/)),
|
||||
),
|
||||
validate: (value) => {
|
||||
if (!value) {
|
||||
return new ValidationResult(false, defineMessage({id: 'admin.support.reportAProblemEmailError', defaultMessage: 'Email is required'}));
|
||||
}
|
||||
return new ValidationResult(true, '');
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'SupportSettings.AllowDownloadLogs',
|
||||
label: defineMessage({id: 'admin.support.problemAllowDownloadTitle', defaultMessage: 'Allow Mobile App Log Downloads:'}),
|
||||
help_text: defineMessage({id: 'admin.support.problemAllowDownloadDescription', defaultMessage: 'When enabled, users can download app logs for troubleshooting. If a ‘Report a Problem’ link is shown, logs can be downloaded as part of that flow; if the ‘Report a Problem’ link is hidden, logs remain accessible as a separate option.'}),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
|
||||
@@ -38,6 +38,13 @@ describe('useExternalLink', () => {
|
||||
expect(queryParams).toEqual({});
|
||||
});
|
||||
|
||||
it('mailto links are untouched even if to mattermost emails', () => {
|
||||
const mailtoURL = 'mailto:example@mattermost.com?subject=123&body=456';
|
||||
const {result: {current: [mailtoHref, mailtoQueryParams]}} = renderHookWithContext(() => useExternalLink(mailtoURL), getBaseState());
|
||||
expect(mailtoHref).toEqual(mailtoURL);
|
||||
expect(mailtoQueryParams).toEqual({});
|
||||
});
|
||||
|
||||
it('all base queries are set correctly', () => {
|
||||
const url = 'https://www.mattermost.com/some/url';
|
||||
const {result: {current: [href, queryParams]}} = renderHookWithContext(() => useExternalLink(url), getBaseState());
|
||||
@@ -129,4 +136,9 @@ describe('useExternalLink', () => {
|
||||
expect(firstHref).toBe(secondHref);
|
||||
expect(firstParams).toBe(secondParams);
|
||||
});
|
||||
it('do not substitute %20 on query params', () => {
|
||||
const url = 'https://www.mattermost.com/some/url?subject=hello%20world';
|
||||
const {result: {current: [href]}} = renderHookWithContext(() => useExternalLink(url), getBaseState());
|
||||
expect(href).toContain('subject=hello%20world');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useExternalLink(href: string, location: string = '', overwriteQu
|
||||
const isCloud = useSelector((state: GlobalState) => getLicense(state).Cloud === 'true');
|
||||
|
||||
return useMemo(() => {
|
||||
if (!href?.includes('mattermost.com')) {
|
||||
if (!href?.includes('mattermost.com') || href?.startsWith('mailto:')) {
|
||||
return [href, {}];
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function useExternalLink(href: string, location: string = '', overwriteQu
|
||||
...overwriteQueryParams,
|
||||
...existingQueryParamsObj,
|
||||
};
|
||||
parsedUrl.search = new URLSearchParams(queryParams).toString();
|
||||
parsedUrl.search = Object.entries(queryParams).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
|
||||
|
||||
return [parsedUrl.toString(), queryParams];
|
||||
}, [href, isCloud, location, overwriteQueryParams, telemetryId, userId]);
|
||||
|
||||
@@ -8,30 +8,26 @@ import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getReportAProblemLink} from 'mattermost-redux/selectors/entities/report_a_problem';
|
||||
import {isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {getUserGuideDropdownPluginMenuItems} from 'selectors/plugins';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import UserGuideDropdown from './user_guide_dropdown';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const {HelpLink, ReportAProblemLink, EnableAskCommunityLink} = getConfig(state);
|
||||
const {HelpLink, EnableAskCommunityLink} = getConfig(state);
|
||||
|
||||
const reportAProblemLink = getReportAProblemLink(state);
|
||||
return {
|
||||
helpLink: HelpLink || '',
|
||||
isMobileView: getIsMobileView(state),
|
||||
reportAProblemLink: ReportAProblemLink || '',
|
||||
reportAProblemLink,
|
||||
enableAskCommunityLink: EnableAskCommunityLink || '',
|
||||
teamUrl: getCurrentRelativeTeamUrl(state),
|
||||
pluginMenuItems: getUserGuideDropdownPluginMenuItems(state),
|
||||
isFirstAdmin: isFirstAdmin(state),
|
||||
onboardingFlowEnabled: getIsOnboardingFlowEnabled(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getConfig,
|
||||
getLicense,
|
||||
} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getReportAProblemLink} from 'mattermost-redux/selectors/entities/report_a_problem';
|
||||
import {
|
||||
getJoinableTeamIds,
|
||||
getCurrentTeam,
|
||||
@@ -34,7 +35,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
const siteName = config.SiteName;
|
||||
const experimentalPrimaryTeam = config.ExperimentalPrimaryTeam;
|
||||
const helpLink = config.HelpLink;
|
||||
const reportAProblemLink = config.ReportAProblemLink;
|
||||
const reportAProblemLink = getReportAProblemLink(state);
|
||||
|
||||
const joinableTeams = getJoinableTeamIds(state);
|
||||
const moreTeamsToJoin = joinableTeams && joinableTeams.length > 0;
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('MobileSidebarRightItems', () => {
|
||||
appDownloadLink: undefined,
|
||||
experimentalPrimaryTeam: undefined,
|
||||
helpLink: undefined,
|
||||
reportAProblemLink: undefined,
|
||||
reportAProblemLink: '',
|
||||
moreTeamsToJoin: false,
|
||||
pluginMenuItems: [],
|
||||
isMentionSearch: false,
|
||||
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
updateSearchTermsForShortcut,
|
||||
showSearchResults,
|
||||
showChannelFiles,
|
||||
showMentions,
|
||||
showFlaggedPosts,
|
||||
closeRightHandSide,
|
||||
updateRhsState,
|
||||
setRhsExpanded,
|
||||
@@ -48,7 +46,6 @@ function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
currentChannel,
|
||||
isRhsExpanded: getIsRhsExpanded(state),
|
||||
isRhsOpen,
|
||||
isSearchingTerm: getIsSearchingTerm(state),
|
||||
searchTerms: getSearchTerms(state),
|
||||
searchTeam: getSearchTeam(state),
|
||||
@@ -82,8 +79,6 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
updateSearchType,
|
||||
showSearchResults,
|
||||
showChannelFiles,
|
||||
showMentions,
|
||||
showFlaggedPosts,
|
||||
setRhsExpanded,
|
||||
closeRightHandSide,
|
||||
autocompleteChannelsForSearch: autocompleteChannels,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useEffect, useState, useRef, useCallback} from 'react';
|
||||
import type {ChangeEvent, MouseEvent, FormEvent} from 'react';
|
||||
import type {ChangeEvent, FormEvent} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getCurrentChannelNameForSearchShortcut} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import HeaderIconWrapper from 'components/channel_header/components/header_icon_wrapper';
|
||||
import UserGuideDropdown from 'components/search/user_guide_dropdown';
|
||||
import SearchBar from 'components/search_bar/search_bar';
|
||||
import SearchHint from 'components/search_hint/search_hint';
|
||||
import SearchResults from 'components/search_results';
|
||||
@@ -18,11 +16,8 @@ import type Provider from 'components/suggestion/provider';
|
||||
import SearchChannelProvider from 'components/suggestion/search_channel_provider';
|
||||
import SearchDateProvider from 'components/suggestion/search_date_provider';
|
||||
import SearchUserProvider from 'components/suggestion/search_user_provider';
|
||||
import FlagIcon from 'components/widgets/icons/flag_icon';
|
||||
import MentionsIcon from 'components/widgets/icons/mentions_icon';
|
||||
import SearchIcon from 'components/widgets/icons/search_icon';
|
||||
import Popover from 'components/widgets/popover';
|
||||
import {ShortcutKeys} from 'components/with_tooltip/tooltip_shortcut';
|
||||
|
||||
import Constants, {searchHintOptions, RHSStates, searchFilesHintOptions} from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
@@ -33,11 +28,6 @@ import type {SearchType} from 'types/store/rhs';
|
||||
|
||||
import type {Props, SearchFilterType} from './types';
|
||||
|
||||
const mentionsShortcut = {
|
||||
default: [ShortcutKeys.ctrl, ShortcutKeys.shift, 'M'],
|
||||
mac: [ShortcutKeys.cmd, ShortcutKeys.shift, 'M'],
|
||||
};
|
||||
|
||||
interface SearchHintOption {
|
||||
searchTerm: string;
|
||||
message: {
|
||||
@@ -87,19 +77,46 @@ const determineVisibleSearchHintOptions = (searchTerms: string, searchType: Sear
|
||||
return newVisibleSearchHintOptions;
|
||||
};
|
||||
|
||||
const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
const {
|
||||
actions,
|
||||
currentChannel,
|
||||
enableFindShortcut,
|
||||
hideSearchBar,
|
||||
isMobileView,
|
||||
searchTerms,
|
||||
searchType,
|
||||
searchTeam,
|
||||
hideMobileSearchBarInRHS,
|
||||
} = props;
|
||||
|
||||
const Search = ({
|
||||
actions: {
|
||||
autocompleteChannelsForSearch,
|
||||
autocompleteUsersInTeam,
|
||||
closeRightHandSide,
|
||||
filterFilesSearchByExt,
|
||||
getMoreFilesForSearch,
|
||||
getMorePostsForSearch,
|
||||
openRHSSearch,
|
||||
setRhsExpanded,
|
||||
showChannelFiles,
|
||||
showSearchResults,
|
||||
updateRhsState,
|
||||
updateSearchTeam,
|
||||
updateSearchTerms,
|
||||
updateSearchTermsForShortcut,
|
||||
updateSearchType,
|
||||
},
|
||||
crossTeamSearchEnabled,
|
||||
hideMobileSearchBarInRHS,
|
||||
isChannelFiles,
|
||||
isFlaggedPosts,
|
||||
isMentionSearch,
|
||||
isMobileView,
|
||||
isPinnedPosts,
|
||||
isRhsExpanded,
|
||||
isSearchingTerm,
|
||||
searchTeam,
|
||||
searchTerms,
|
||||
searchType,
|
||||
searchVisible,
|
||||
channelDisplayName,
|
||||
children,
|
||||
currentChannel,
|
||||
enableFindShortcut,
|
||||
getFocus,
|
||||
hideSearchBar,
|
||||
isSideBarRight,
|
||||
isSideBarRightOpen,
|
||||
}: Props): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
const currentChannelName = useSelector(getCurrentChannelNameForSearchShortcut);
|
||||
|
||||
@@ -116,8 +133,8 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
|
||||
const suggestionProviders = useRef<Provider[]>([
|
||||
new SearchDateProvider(),
|
||||
new SearchChannelProvider(actions.autocompleteChannelsForSearch),
|
||||
new SearchUserProvider(actions.autocompleteUsersInTeam),
|
||||
new SearchChannelProvider(autocompleteChannelsForSearch),
|
||||
new SearchUserProvider(autocompleteUsersInTeam),
|
||||
]);
|
||||
|
||||
const isDesktop = isDesktopApp() && isServerVersionGreaterThanOrEqualTo(getDesktopVersion(), '4.7.0');
|
||||
@@ -139,11 +156,11 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
|
||||
e.preventDefault();
|
||||
if (hideSearchBar) {
|
||||
actions.openRHSSearch();
|
||||
openRHSSearch();
|
||||
setKeepInputFocused(true);
|
||||
}
|
||||
if (currentChannelName) {
|
||||
actions.updateSearchTermsForShortcut();
|
||||
updateSearchTermsForShortcut();
|
||||
}
|
||||
handleFocus();
|
||||
}
|
||||
@@ -156,10 +173,10 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
}, [hideSearchBar, currentChannelName]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (isMobileView && props.isSideBarRight) {
|
||||
if (isMobileView && isSideBarRight) {
|
||||
handleFocus();
|
||||
}
|
||||
}, [isMobileView, props.isSideBarRight]);
|
||||
}, [isMobileView, isSideBarRight]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (!isMobileView) {
|
||||
@@ -173,24 +190,24 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
}
|
||||
}, [isMobileView, searchTerms]);
|
||||
|
||||
const getMorePostsForSearch = useCallback(() => {
|
||||
const getMorePostsForSearchCallback = useCallback(() => {
|
||||
let team = searchTeam;
|
||||
if (props.isMentionSearch) {
|
||||
if (isMentionSearch) {
|
||||
team = '';
|
||||
}
|
||||
props.actions.getMorePostsForSearch(team);
|
||||
}, [searchTeam, props.actions, props.isMentionSearch]);
|
||||
getMorePostsForSearch(team);
|
||||
}, [searchTeam, isMentionSearch, getMorePostsForSearch]);
|
||||
|
||||
const getMoreFilesForSearch = useCallback(() => {
|
||||
const getMoreFilesForSearchCallback = useCallback(() => {
|
||||
let team = searchTeam;
|
||||
if (props.isMentionSearch) {
|
||||
if (isMentionSearch) {
|
||||
team = '';
|
||||
}
|
||||
props.actions.getMoreFilesForSearch(team);
|
||||
}, [searchTeam, props.actions]);
|
||||
getMoreFilesForSearch(team);
|
||||
}, [searchTeam, isMentionSearch, getMoreFilesForSearch]);
|
||||
|
||||
// handle cloding of rhs-flyout
|
||||
const handleClose = (): void => actions.closeRightHandSide();
|
||||
const handleClose = (): void => closeRightHandSide();
|
||||
|
||||
// focus the search input
|
||||
const handleFocus = (): void => setFocused(true);
|
||||
@@ -231,13 +248,13 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
};
|
||||
|
||||
const handleUpdateSearchTeamFromResult = async (teamId: string) => {
|
||||
actions.updateSearchTeam(teamId);
|
||||
updateSearchTeam(teamId);
|
||||
const newTerms = searchTerms.
|
||||
replace(/\bin:[^\s]*/gi, '').replace(/\s{2,}/g, ' ').
|
||||
replace(/\bfrom:[^\s]*/gi, '').replace(/\s{2,}/g, ' ');
|
||||
|
||||
if (newTerms.trim() !== searchTerms.trim()) {
|
||||
actions.updateSearchTerms(newTerms);
|
||||
updateSearchTerms(newTerms);
|
||||
}
|
||||
|
||||
handleSearch().then(() => {
|
||||
@@ -247,12 +264,12 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
};
|
||||
|
||||
const handleUpdateSearchTerms = (terms: string): void => {
|
||||
actions.updateSearchTerms(terms);
|
||||
updateSearchTerms(terms);
|
||||
updateHighlightedSearchHint();
|
||||
};
|
||||
|
||||
const handleOnSearchTypeSelected = (searchType || searchTerms) ? undefined : (value: SearchType) => {
|
||||
actions.updateSearchType(value);
|
||||
updateSearchType(value);
|
||||
if (!searchType) {
|
||||
setDropdownFocused(false);
|
||||
}
|
||||
@@ -261,7 +278,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
const term = e.target.value;
|
||||
actions.updateSearchTerms(term);
|
||||
updateSearchTerms(term);
|
||||
};
|
||||
|
||||
// call this function without parameters to reset `SearchHint`
|
||||
@@ -299,7 +316,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
if (indexChangedViaKeyPress) {
|
||||
setKeepInputFocused(true);
|
||||
if (!searchType && !searchTerms) {
|
||||
actions.updateSearchType(highlightedSearchHintIndex === 0 ? 'messages' : 'files');
|
||||
updateSearchType(highlightedSearchHintIndex === 0 ? 'messages' : 'files');
|
||||
setHighlightedSearchHintIndex(-1);
|
||||
} else {
|
||||
handleAddSearchTerm(visibleSearchHintOptions[highlightedSearchHintIndex].searchTerm);
|
||||
@@ -307,8 +324,8 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.isMentionSearch) {
|
||||
actions.updateRhsState(RHSStates.SEARCH);
|
||||
if (isMentionSearch) {
|
||||
updateRhsState(RHSStates.SEARCH);
|
||||
}
|
||||
|
||||
handleSearch().then(() => {
|
||||
@@ -333,7 +350,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
return;
|
||||
}
|
||||
|
||||
const {error} = await actions.showSearchResults(Boolean(props.isMentionSearch)) as any;
|
||||
const {error} = await showSearchResults(Boolean(isMentionSearch)) as any;
|
||||
|
||||
if (!error) {
|
||||
handleSearchOnSuccess();
|
||||
@@ -347,50 +364,50 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
};
|
||||
|
||||
const handleClear = (): void => {
|
||||
if (props.isMentionSearch) {
|
||||
if (isMentionSearch) {
|
||||
setFocused(false);
|
||||
actions.updateRhsState(RHSStates.SEARCH);
|
||||
updateRhsState(RHSStates.SEARCH);
|
||||
}
|
||||
actions.updateSearchTerms('');
|
||||
actions.updateSearchTeam(null);
|
||||
actions.updateSearchType('');
|
||||
updateSearchTerms('');
|
||||
updateSearchTeam(null);
|
||||
updateSearchType('');
|
||||
};
|
||||
|
||||
const handleShrink = (): void => {
|
||||
props.actions.setRhsExpanded(false);
|
||||
setRhsExpanded(false);
|
||||
};
|
||||
|
||||
const handleSetSearchFilter = (filterType: SearchFilterType): void => {
|
||||
switch (filterType) {
|
||||
case 'documents':
|
||||
props.actions.filterFilesSearchByExt(['doc', 'pdf', 'docx', 'odt', 'rtf', 'txt']);
|
||||
filterFilesSearchByExt(['doc', 'pdf', 'docx', 'odt', 'rtf', 'txt']);
|
||||
break;
|
||||
case 'spreadsheets':
|
||||
props.actions.filterFilesSearchByExt(['xls', 'xlsx', 'ods']);
|
||||
filterFilesSearchByExt(['xls', 'xlsx', 'ods']);
|
||||
break;
|
||||
case 'presentations':
|
||||
props.actions.filterFilesSearchByExt(['ppt', 'pptx', 'odp']);
|
||||
filterFilesSearchByExt(['ppt', 'pptx', 'odp']);
|
||||
break;
|
||||
case 'code':
|
||||
props.actions.filterFilesSearchByExt(['py', 'go', 'java', 'kt', 'c', 'cpp', 'h', 'html', 'js', 'ts', 'cs', 'vb', 'php', 'pl', 'r', 'rb', 'sql', 'swift', 'json']);
|
||||
filterFilesSearchByExt(['py', 'go', 'java', 'kt', 'c', 'cpp', 'h', 'html', 'js', 'ts', 'cs', 'vb', 'php', 'pl', 'r', 'rb', 'sql', 'swift', 'json']);
|
||||
break;
|
||||
case 'images':
|
||||
props.actions.filterFilesSearchByExt(['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'svg', 'psd', 'xcf']);
|
||||
filterFilesSearchByExt(['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'svg', 'psd', 'xcf']);
|
||||
break;
|
||||
case 'audio':
|
||||
props.actions.filterFilesSearchByExt(['ogg', 'mp3', 'wav', 'flac']);
|
||||
filterFilesSearchByExt(['ogg', 'mp3', 'wav', 'flac']);
|
||||
break;
|
||||
case 'video':
|
||||
props.actions.filterFilesSearchByExt(['ogm', 'mp4', 'avi', 'webm', 'mov', 'mkv', 'mpeg', 'mpg']);
|
||||
filterFilesSearchByExt(['ogm', 'mp4', 'avi', 'webm', 'mov', 'mkv', 'mpeg', 'mpg']);
|
||||
break;
|
||||
default:
|
||||
props.actions.filterFilesSearchByExt([]);
|
||||
filterFilesSearchByExt([]);
|
||||
}
|
||||
setSearchFilterType(filterType);
|
||||
if (props.isChannelFiles && currentChannel) {
|
||||
props.actions.showChannelFiles(currentChannel.id);
|
||||
if (isChannelFiles && currentChannel) {
|
||||
showChannelFiles(currentChannel.id);
|
||||
} else {
|
||||
props.actions.showSearchResults(false);
|
||||
showSearchResults(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -399,64 +416,12 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
setIndexChangedViaKeyPress(false);
|
||||
};
|
||||
|
||||
const searchMentions = (e: MouseEvent<HTMLButtonElement>): void => {
|
||||
e.preventDefault();
|
||||
if (props.isMentionSearch) {
|
||||
actions.closeRightHandSide();
|
||||
return;
|
||||
}
|
||||
actions.showMentions();
|
||||
};
|
||||
|
||||
const getFlagged = (e: MouseEvent<HTMLButtonElement>): void => {
|
||||
e.preventDefault();
|
||||
if (props.isFlaggedPosts) {
|
||||
actions.closeRightHandSide();
|
||||
return;
|
||||
}
|
||||
actions.showFlaggedPosts();
|
||||
};
|
||||
|
||||
const searchButtonClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
actions.openRHSSearch();
|
||||
openRHSSearch();
|
||||
};
|
||||
|
||||
const renderMentionButton = (): JSX.Element => (
|
||||
<HeaderIconWrapper
|
||||
buttonClass={classNames(
|
||||
'channel-header__icon',
|
||||
{'channel-header__icon--active': props.isMentionSearch},
|
||||
)}
|
||||
buttonId={props.isSideBarRight ? 'sbrChannelHeaderMentionButton' : 'channelHeaderMentionButton'}
|
||||
onClick={searchMentions}
|
||||
tooltip={intl.formatMessage({id: 'channel_header.recentMentions', defaultMessage: 'Recent mentions'})}
|
||||
tooltipShortcut={mentionsShortcut}
|
||||
isRhsOpen={props.isRhsOpen}
|
||||
>
|
||||
<MentionsIcon
|
||||
className='icon icon--standard'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</HeaderIconWrapper>
|
||||
);
|
||||
|
||||
const renderFlagBtn = (): JSX.Element => (
|
||||
<HeaderIconWrapper
|
||||
buttonClass={classNames(
|
||||
'channel-header__icon ',
|
||||
{'channel-header__icon--active': props.isFlaggedPosts},
|
||||
)}
|
||||
buttonId={props.isSideBarRight ? 'sbrChannelHeaderFlagButton' : 'channelHeaderFlagButton'}
|
||||
onClick={getFlagged}
|
||||
tooltip={intl.formatMessage({id: 'channel_header.flagged', defaultMessage: 'Saved messages'})}
|
||||
isRhsOpen={props.isRhsOpen}
|
||||
>
|
||||
<FlagIcon className='icon icon--standard'/>
|
||||
</HeaderIconWrapper>
|
||||
);
|
||||
|
||||
const renderHintPopover = (): JSX.Element => {
|
||||
let termsUsed = 0;
|
||||
|
||||
@@ -470,7 +435,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
}
|
||||
});
|
||||
|
||||
if (visibleSearchHintOptions.length === 0 || props.isMentionSearch) {
|
||||
if (visibleSearchHintOptions.length === 0 || isMentionSearch) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -478,7 +443,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
id={`${props.isSideBarRight ? 'sbr-' : ''}searchbar-help-popup`}
|
||||
id={`${isSideBarRight ? 'sbr-' : ''}searchbar-help-popup`}
|
||||
placement='bottom'
|
||||
className={helpClass}
|
||||
>
|
||||
@@ -502,7 +467,7 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
<>
|
||||
<div className='sidebar-collapse__container'>
|
||||
<div
|
||||
id={props.isSideBarRight ? 'sbrSidebarCollapse' : 'sidebarCollapse'}
|
||||
id={isSideBarRight ? 'sbrSidebarCollapse' : 'sidebarCollapse'}
|
||||
className='sidebar-collapse'
|
||||
onClick={handleClose}
|
||||
>
|
||||
@@ -524,20 +489,20 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
setKeepFocused={setKeepInputFocused}
|
||||
isFocused={focused}
|
||||
suggestionProviders={suggestionProviders.current}
|
||||
isSideBarRight={props.isSideBarRight}
|
||||
isSearchingTerm={props.isSearchingTerm}
|
||||
getFocus={props.getFocus}
|
||||
isSideBarRight={isSideBarRight}
|
||||
isSearchingTerm={isSearchingTerm}
|
||||
getFocus={getFocus}
|
||||
searchTerms={searchTerms}
|
||||
searchType={searchType}
|
||||
clearSearchType={() => actions.updateSearchType('')}
|
||||
clearSearchType={() => updateSearchType('')}
|
||||
>
|
||||
{!props.isMobileView && renderHintPopover()}
|
||||
{!isMobileView && renderHintPopover()}
|
||||
</SearchBar>
|
||||
</>
|
||||
);
|
||||
|
||||
// when inserted in RHSSearchNav component, just return SearchBar
|
||||
if (!props.isSideBarRight) {
|
||||
if (!isSideBarRight) {
|
||||
if (hideSearchBar) {
|
||||
return (
|
||||
<HeaderIconWrapper
|
||||
@@ -571,34 +536,31 @@ const Search: React.FC<Props> = (props: Props): JSX.Element => {
|
||||
<div className='search-bar__container channel-header alt'>
|
||||
<div className='sidebar-right__table'>
|
||||
{renderSearchBar()}
|
||||
{renderMentionButton()}
|
||||
{renderFlagBtn()}
|
||||
<UserGuideDropdown/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{props.searchVisible ? (
|
||||
{searchVisible ? (
|
||||
<SearchResults
|
||||
isMentionSearch={props.isMentionSearch}
|
||||
isFlaggedPosts={props.isFlaggedPosts}
|
||||
isPinnedPosts={props.isPinnedPosts}
|
||||
isChannelFiles={props.isChannelFiles}
|
||||
isMentionSearch={isMentionSearch}
|
||||
isFlaggedPosts={isFlaggedPosts}
|
||||
isPinnedPosts={isPinnedPosts}
|
||||
isChannelFiles={isChannelFiles}
|
||||
shrink={handleShrink}
|
||||
channelDisplayName={props.channelDisplayName}
|
||||
isOpened={props.isSideBarRightOpen}
|
||||
channelDisplayName={channelDisplayName}
|
||||
isOpened={isSideBarRightOpen}
|
||||
updateSearchTerms={handleAddSearchTerm}
|
||||
updateSearchTeam={handleUpdateSearchTeamFromResult}
|
||||
handleSearchHintSelection={handleSearchHintSelection}
|
||||
isSideBarExpanded={props.isRhsExpanded}
|
||||
getMorePostsForSearch={getMorePostsForSearch}
|
||||
getMoreFilesForSearch={getMoreFilesForSearch}
|
||||
isSideBarExpanded={isRhsExpanded}
|
||||
getMorePostsForSearch={getMorePostsForSearchCallback}
|
||||
getMoreFilesForSearch={getMoreFilesForSearchCallback}
|
||||
setSearchFilterType={handleSetSearchFilter}
|
||||
searchFilterType={searchFilterType}
|
||||
setSearchType={(value: SearchType) => actions.updateSearchType(value)}
|
||||
setSearchType={(value: SearchType) => updateSearchType(value)}
|
||||
searchType={searchType || 'messages'}
|
||||
crossTeamSearchEnabled={props.crossTeamSearchEnabled}
|
||||
crossTeamSearchEnabled={crossTeamSearchEnabled}
|
||||
/>
|
||||
) : props.children}
|
||||
) : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,7 +24,6 @@ export type OwnProps = {
|
||||
|
||||
export type StateProps = {
|
||||
isRhsExpanded: boolean;
|
||||
isRhsOpen: boolean;
|
||||
isSearchingTerm: boolean;
|
||||
searchTerms: string;
|
||||
searchTeam: string;
|
||||
@@ -48,8 +47,6 @@ export type DispatchProps = {
|
||||
updateSearchType: (searchType: string) => Action;
|
||||
showSearchResults: (isMentionSearch: boolean) => unknown;
|
||||
showChannelFiles: (channelId: string) => void;
|
||||
showMentions: () => void;
|
||||
showFlaggedPosts: () => void;
|
||||
setRhsExpanded: (expanded: boolean) => Action;
|
||||
closeRightHandSide: () => void;
|
||||
autocompleteChannelsForSearch: (term: string, teamId: string, success?: (channels: Channel[]) => void, error?: (err: ServerError) => void) => void;
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UserGuideDropdown should match snapshot 1`] = `
|
||||
<MenuWrapper
|
||||
animationComponent={[Function]}
|
||||
className="userGuideHelp"
|
||||
onToggle={[Function]}
|
||||
>
|
||||
<WithTooltip
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Help"
|
||||
id="channel_header.userHelpGuide"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
aria-expanded="true"
|
||||
className="channel-header__icon"
|
||||
id="channelHeaderUserGuideButton"
|
||||
type="button"
|
||||
>
|
||||
<UserGuideIcon
|
||||
className="icon"
|
||||
/>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
<Menu
|
||||
ariaLabel="Add Channel Dropdown"
|
||||
id="AddChannelDropdown"
|
||||
openLeft={true}
|
||||
openUp={false}
|
||||
>
|
||||
<Memo(MenuGroup)>
|
||||
<MenuItemExternalLink
|
||||
id="askTheCommunityLink"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Ask the community"
|
||||
url="https://mattermost.com/pl/default-ask-mattermost-community/"
|
||||
/>
|
||||
<MenuItemExternalLink
|
||||
id="helpResourcesLink"
|
||||
show={true}
|
||||
text="Help resources"
|
||||
url="helpLink"
|
||||
/>
|
||||
<MenuItemExternalLink
|
||||
id="reportAProblemLink"
|
||||
show={true}
|
||||
text="Report a problem"
|
||||
url="reportAProblemLink"
|
||||
/>
|
||||
<MenuItemAction
|
||||
id="keyboardShortcuts"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Keyboard shortcuts"
|
||||
/>
|
||||
</Memo(MenuGroup)>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
`;
|
||||
|
||||
exports[`UserGuideDropdown should match snapshot for false of enableAskCommunityLink 1`] = `
|
||||
<MenuWrapper
|
||||
animationComponent={[Function]}
|
||||
className="userGuideHelp"
|
||||
onToggle={[Function]}
|
||||
>
|
||||
<WithTooltip
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Help"
|
||||
id="channel_header.userHelpGuide"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<button
|
||||
aria-expanded="true"
|
||||
className="channel-header__icon"
|
||||
id="channelHeaderUserGuideButton"
|
||||
type="button"
|
||||
>
|
||||
<UserGuideIcon
|
||||
className="icon"
|
||||
/>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
<Menu
|
||||
ariaLabel="Add Channel Dropdown"
|
||||
id="AddChannelDropdown"
|
||||
openLeft={true}
|
||||
openUp={false}
|
||||
>
|
||||
<Memo(MenuGroup)>
|
||||
<MenuItemExternalLink
|
||||
id="helpResourcesLink"
|
||||
show={true}
|
||||
text="Help resources"
|
||||
url="helpLink"
|
||||
/>
|
||||
<MenuItemExternalLink
|
||||
id="reportAProblemLink"
|
||||
show={true}
|
||||
text="Report a problem"
|
||||
url="reportAProblemLink"
|
||||
/>
|
||||
<MenuItemAction
|
||||
id="keyboardShortcuts"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Keyboard shortcuts"
|
||||
/>
|
||||
</Memo(MenuGroup)>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
`;
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import UserGuideDropdown from './user_guide_dropdown';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const {HelpLink, ReportAProblemLink, EnableAskCommunityLink} = getConfig(state);
|
||||
return {
|
||||
helpLink: HelpLink!,
|
||||
reportAProblemLink: ReportAProblemLink!,
|
||||
enableAskCommunityLink: EnableAskCommunityLink!,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
openModal,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export default connector(UserGuideDropdown);
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
|
||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
import UserGuideDropdown from './user_guide_dropdown';
|
||||
|
||||
jest.mock('actions/telemetry_actions.jsx', () => {
|
||||
const original = jest.requireActual('actions/telemetry_actions.jsx');
|
||||
|
||||
return {
|
||||
...original,
|
||||
trackEvent: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('UserGuideDropdown', () => {
|
||||
const baseProps = {
|
||||
helpLink: 'helpLink',
|
||||
reportAProblemLink: 'reportAProblemLink',
|
||||
enableAskCommunityLink: 'true',
|
||||
actions: {
|
||||
openModal: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<UserGuideDropdown {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot for false of enableAskCommunityLink', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
enableAskCommunityLink: 'false',
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<UserGuideDropdown {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should set state buttonActive on toggle of MenuWrapper', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<UserGuideDropdown {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state('buttonActive')).toBe(false);
|
||||
wrapper.find(MenuWrapper).prop('onToggle')!(true);
|
||||
expect(wrapper.state('buttonActive')).toBe(true);
|
||||
});
|
||||
|
||||
test('Should set state buttonActive on toggle of MenuWrapper', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<UserGuideDropdown {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.find(Menu.ItemAction).prop('onClick')({preventDefault: jest.fn()});
|
||||
expect(baseProps.actions.openModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Should call for track event on click of askTheCommunityLink', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<UserGuideDropdown {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.find(Menu.ItemExternalLink).find('#askTheCommunityLink').prop('onClick')!({} as unknown as React.MouseEvent);
|
||||
expect(trackEvent).toBeCalledWith('ui', 'help_ask_the_community');
|
||||
});
|
||||
});
|
||||
@@ -1,126 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import {FormattedMessage, injectIntl} from 'react-intl';
|
||||
import type {WrappedComponentProps} from 'react-intl';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import KeyboardShortcutsModal from 'components/keyboard_shortcuts/keyboard_shortcuts_modal/keyboard_shortcuts_modal';
|
||||
import UserGuideIcon from 'components/widgets/icons/user_guide_icon';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import type {PropsFromRedux} from './index';
|
||||
|
||||
const askTheCommunityUrl = 'https://mattermost.com/pl/default-ask-mattermost-community/';
|
||||
|
||||
type Props = PropsFromRedux & WrappedComponentProps
|
||||
|
||||
type State = {
|
||||
buttonActive: boolean;
|
||||
};
|
||||
|
||||
class UserGuideDropdown extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
buttonActive: false,
|
||||
};
|
||||
}
|
||||
|
||||
openKeyboardShortcutsModal = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.props.actions.openModal({
|
||||
modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL,
|
||||
dialogType: KeyboardShortcutsModal,
|
||||
});
|
||||
};
|
||||
|
||||
buttonToggleState = (menuActive: boolean) => {
|
||||
this.setState({
|
||||
buttonActive: menuActive,
|
||||
});
|
||||
};
|
||||
|
||||
askTheCommunityClick = () => {
|
||||
trackEvent('ui', 'help_ask_the_community');
|
||||
};
|
||||
|
||||
renderDropdownItems = (): React.ReactNode => {
|
||||
const {intl} = this.props;
|
||||
|
||||
return (
|
||||
<Menu.Group>
|
||||
{this.props.enableAskCommunityLink === 'true' && (
|
||||
<Menu.ItemExternalLink
|
||||
id='askTheCommunityLink'
|
||||
url={askTheCommunityUrl}
|
||||
text={intl.formatMessage({id: 'userGuideHelp.askTheCommunity', defaultMessage: 'Ask the community'})}
|
||||
onClick={this.askTheCommunityClick}
|
||||
/>
|
||||
)}
|
||||
<Menu.ItemExternalLink
|
||||
id='helpResourcesLink'
|
||||
url={this.props.helpLink}
|
||||
text={intl.formatMessage({id: 'userGuideHelp.helpResources', defaultMessage: 'Help resources'})}
|
||||
/>
|
||||
<Menu.ItemExternalLink
|
||||
id='reportAProblemLink'
|
||||
url={this.props.reportAProblemLink}
|
||||
text={intl.formatMessage({id: 'userGuideHelp.reportAProblem', defaultMessage: 'Report a problem'})}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
id='keyboardShortcuts'
|
||||
onClick={this.openKeyboardShortcutsModal}
|
||||
text={intl.formatMessage({id: 'userGuideHelp.keyboardShortcuts', defaultMessage: 'Keyboard shortcuts'})}
|
||||
/>
|
||||
</Menu.Group>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {intl} = this.props;
|
||||
const tooltipText = (
|
||||
<FormattedMessage
|
||||
id={'channel_header.userHelpGuide'}
|
||||
defaultMessage='Help'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuWrapper
|
||||
className='userGuideHelp'
|
||||
onToggle={this.buttonToggleState}
|
||||
>
|
||||
<WithTooltip
|
||||
title={this.state.buttonActive ? '' : tooltipText}
|
||||
>
|
||||
<button
|
||||
id='channelHeaderUserGuideButton'
|
||||
className={classNames('channel-header__icon', {'channel-header__icon--active': this.state.buttonActive})}
|
||||
type='button'
|
||||
aria-expanded='true'
|
||||
>
|
||||
<UserGuideIcon className='icon'/>
|
||||
</button>
|
||||
</WithTooltip>
|
||||
<Menu
|
||||
openLeft={true}
|
||||
openUp={false}
|
||||
id='AddChannelDropdown'
|
||||
ariaLabel={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.dropdownAriaLabel', defaultMessage: 'Add Channel Dropdown'})}
|
||||
>
|
||||
{this.renderDropdownItems()}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(UserGuideDropdown);
|
||||
@@ -2577,8 +2577,22 @@
|
||||
"admin.support.helpTitle": "Help Link:",
|
||||
"admin.support.privacyDesc": "The URL for the Privacy link on the login and sign-up pages. If this field is empty, the Privacy link is hidden from users.",
|
||||
"admin.support.privacyTitle": "Privacy Policy Link:",
|
||||
"admin.support.problemDesc": "The URL for the Report a Problem link in the Help Menu. If this field is empty, the link is removed from the Help Menu.",
|
||||
"admin.support.problemTitle": "Report a Problem Link:",
|
||||
"admin.support.problemAllowDownloadDescription": "When enabled, users can download app logs for troubleshooting. If a ‘Report a Problem’ link is shown, logs can be downloaded as part of that flow; if the ‘Report a Problem’ link is hidden, logs remain accessible as a separate option.",
|
||||
"admin.support.problemAllowDownloadTitle": "Allow Mobile App Log Downloads:",
|
||||
"admin.support.problemType.customLink": "Custom link",
|
||||
"admin.support.problemType.defaultLink": "Default link",
|
||||
"admin.support.problemType.email": "Email address",
|
||||
"admin.support.problemType.hide": "Hide link",
|
||||
"admin.support.reportAProblemDefaultLinkDescription": "Users will be directed to this link when they choose ‘Report a Problem’.",
|
||||
"admin.support.reportAProblemDefaultLinkTitle": "Default Report a Problem Link:",
|
||||
"admin.support.reportAProblemEmailDescription": "Enter the email address that users will be prompted to send a message to when they choose ‘Report a Problem’.",
|
||||
"admin.support.reportAProblemEmailError": "Email is required",
|
||||
"admin.support.reportAProblemEmailTitle": "Report a Problem Email Address:",
|
||||
"admin.support.reportAProblemLinkDescription": "Enter the URL that users will be directed to when they choose ‘Report a Problem’.",
|
||||
"admin.support.reportAProblemLinkError": "Link is required",
|
||||
"admin.support.reportAProblemLinkTitle": "Custom Report a Problem Link:",
|
||||
"admin.support.reportAProblemTypeDescription": "Select how the ‘Report a Problem’ option behaves. Choosing ‘Custom link’ or ‘Email address’ allows you to provide a URL or address in the next field. ‘Hide link’ removes the ‘Report a Problem’ option from the app.",
|
||||
"admin.support.reportAProblemTypeTitle": "Report a Problem:",
|
||||
"admin.support.termsDesc": "Link to the terms under which users may use your online service. By default, this includes the \"Mattermost Acceptable Use Policy\" explaining the terms under which Mattermost software is provided to end users. If you change the default link to add your own terms for using the service you provide, your new terms must include a link to the default terms so end users are aware of the Mattermost Acceptable Use Policy for Mattermost software.",
|
||||
"admin.support.termsOfServiceReAcceptanceHelp": "The number of days before Terms of Service acceptance expires, and the terms must be re-accepted.",
|
||||
"admin.support.termsOfServiceReAcceptanceTitle": "Re-Acceptance Period:",
|
||||
@@ -6067,7 +6081,6 @@
|
||||
"userAccountMenu.setCustomStatusMenuItem.noStatusSet": "Set custom status",
|
||||
"userAccountMenu.setCustomStatusMenuItem.noStatusTextSet": "Set custom status text",
|
||||
"userGuideHelp.askTheCommunity": "Ask the community",
|
||||
"userGuideHelp.helpResources": "Help resources",
|
||||
"userGuideHelp.keyboardShortcuts": "Keyboard shortcuts",
|
||||
"userGuideHelp.mattermostUserGuide": "Mattermost user guide",
|
||||
"userGuideHelp.reportAProblem": "Report a problem",
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {getBrowserInfo} from 'mattermost-redux/utils/browser_info';
|
||||
|
||||
import {getReportAProblemLink, getSystemInfoMailtoLink} from './report_a_problem';
|
||||
|
||||
jest.mock('mattermost-redux/utils/browser_info', () => ({
|
||||
getBrowserInfo: jest.fn().mockReturnValue({browser: 'Chrome', browserVersion: '1.0.0'}),
|
||||
getPlatformInfo: jest.fn().mockReturnValue('macOS'),
|
||||
}));
|
||||
|
||||
describe('getReportAProblemLink', () => {
|
||||
it('should return empty when invalid type', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
ReportAProblemType: 'invalid',
|
||||
ReportAProblemLink: 'https://example.com/report',
|
||||
ReportAProblemMail: 'test@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
expect(getReportAProblemLink(state)).toEqual('');
|
||||
});
|
||||
|
||||
it('should return the value of the link', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
ReportAProblemType: 'link',
|
||||
ReportAProblemLink: 'https://example.com/report',
|
||||
ReportAProblemMail: 'test@example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
expect(getReportAProblemLink(state)).toEqual('https://example.com/report');
|
||||
state.entities.general.config.ReportAProblemLink = 'https://example.com/new-report';
|
||||
expect(getReportAProblemLink(state)).toEqual('https://example.com/new-report');
|
||||
});
|
||||
|
||||
it('should return the value of the mail', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: '123',
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: '456',
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
ReportAProblemType: 'email',
|
||||
ReportAProblemLink: 'https://example.com/report',
|
||||
ReportAProblemMail: 'test@example.com',
|
||||
Version: '1.0.0',
|
||||
BuildNumber: '12345',
|
||||
SiteName: 'Example',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
const link = getReportAProblemLink(state);
|
||||
expect(link).toContain(`mailto:test@example.com?subject=${encodeURIComponent('Problem with Example app')}&body=${encodeURIComponent('System Information:')}`);
|
||||
expect(link).toContain(encodeURIComponent('- User ID: 123'));
|
||||
expect(link).toContain(encodeURIComponent('- Team ID: 456'));
|
||||
expect(link).toContain(encodeURIComponent('- Server Version: 1.0.0 (12345)'));
|
||||
expect(link).toContain(encodeURIComponent('- Browser: Chrome 1.0.0'));
|
||||
expect(link).toContain(encodeURIComponent('- Platform: macOS'));
|
||||
});
|
||||
|
||||
it('should return the default value if licensed', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
ReportAProblemType: 'default',
|
||||
ReportAProblemLink: 'https://example.com/report',
|
||||
ReportAProblemMail: 'test@example.com',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
expect(getReportAProblemLink(state)).toContain('https://mattermost.com/pl/report_a_problem_licensed');
|
||||
});
|
||||
|
||||
it('should return the default value if unlicensed', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
ReportAProblemType: 'default',
|
||||
ReportAProblemLink: 'https://example.com/report',
|
||||
ReportAProblemMail: 'test@example.com',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'false',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
expect(getReportAProblemLink(state)).toContain('https://mattermost.com/pl/report_a_problem_unlicensed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSystemInfoMailtoLink', () => {
|
||||
it('should return the link with the correct data', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: '123',
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: '456',
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
Version: '1.0.0',
|
||||
BuildNumber: '12345',
|
||||
SiteName: 'Example',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
const link = getSystemInfoMailtoLink(state, 'test@example.com');
|
||||
expect(link).toContain(`mailto:test@example.com?subject=${encodeURIComponent('Problem with Example app')}&body=${encodeURIComponent('System Information:')}`);
|
||||
expect(link).toContain(encodeURIComponent('- User ID: 123'));
|
||||
expect(link).toContain(encodeURIComponent('- Team ID: 456'));
|
||||
expect(link).toContain(encodeURIComponent('- Server Version: 1.0.0 (12345)'));
|
||||
expect(link).toContain(encodeURIComponent('- Browser: Chrome 1.0.0'));
|
||||
expect(link).toContain(encodeURIComponent('- Platform: macOS'));
|
||||
});
|
||||
|
||||
it('should only execute once if called with the same values', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: '123',
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: '456',
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
Version: '1.0.0',
|
||||
BuildNumber: '12345',
|
||||
SiteName: 'Example',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
|
||||
getSystemInfoMailtoLink(state, 'test@example1.com');
|
||||
expect(getBrowserInfo).toHaveBeenCalledTimes(1);
|
||||
|
||||
getSystemInfoMailtoLink(state, 'test@example1.com');
|
||||
expect(getBrowserInfo).toHaveBeenCalledTimes(1); // No new calls
|
||||
|
||||
getSystemInfoMailtoLink(state, 'test@example2.com');
|
||||
expect(getBrowserInfo).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||
import {getBrowserInfo, getPlatformInfo} from 'mattermost-redux/utils/browser_info';
|
||||
|
||||
import {getConfig, getLicense} from './general';
|
||||
import {getCurrentTeamId} from './teams';
|
||||
import {getCurrentUserId} from './users';
|
||||
|
||||
export function getReportAProblemLink(state: GlobalState): string {
|
||||
const config = getConfig(state);
|
||||
const type = config.ReportAProblemType;
|
||||
switch (type) {
|
||||
case 'email':
|
||||
return getSystemInfoMailtoLink(state, config.ReportAProblemMail);
|
||||
case 'link':
|
||||
if (config.ReportAProblemLink) {
|
||||
return config.ReportAProblemLink;
|
||||
}
|
||||
|
||||
// falls through
|
||||
case 'default': {
|
||||
const isLicensed = getLicense(state).IsLicensed === 'true';
|
||||
if (isLicensed) {
|
||||
return 'https://mattermost.com/pl/report_a_problem_licensed';
|
||||
}
|
||||
return 'https://mattermost.com/pl/report_a_problem_unlicensed';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export const getSystemInfoMailtoLink = createSelector(
|
||||
'getSystemInfoMailtoLink',
|
||||
getCurrentUserId,
|
||||
getCurrentTeamId,
|
||||
(state: GlobalState) => getConfig(state).Version,
|
||||
(state: GlobalState) => getConfig(state).BuildNumber,
|
||||
(state: GlobalState) => getConfig(state).SiteName,
|
||||
(state: GlobalState, supportEmail: string | undefined) => supportEmail,
|
||||
(currentUserId: string, currentTeamId: string, version: string | undefined, buildNumber: string | undefined, siteName: string | undefined, supportEmail: string | undefined) => {
|
||||
const {browser, browserVersion} = getBrowserInfo();
|
||||
const platformName = getPlatformInfo();
|
||||
|
||||
const subject = `Problem with ${siteName || 'Mattermost'} app`;
|
||||
const body = `
|
||||
System Information:
|
||||
- User ID: ${currentUserId}
|
||||
- Team ID: ${currentTeamId}
|
||||
- Server Version: ${version} (${buildNumber})
|
||||
- Browser: ${browser} ${browserVersion}
|
||||
- Platform: ${platformName}
|
||||
`.trim();
|
||||
|
||||
return `mailto:${supportEmail || ''}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getBrowserInfo, getPlatformInfo} from './browser_info';
|
||||
|
||||
describe('utils/browser_info', () => {
|
||||
const originalNavigator = window.navigator;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a mock navigator object
|
||||
Object.defineProperty(window, 'navigator', {
|
||||
value: {
|
||||
userAgent: '',
|
||||
platform: '',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore the original navigator
|
||||
Object.defineProperty(window, 'navigator', {
|
||||
value: originalNavigator,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBrowserInfo', () => {
|
||||
const browserTestCases = [
|
||||
{
|
||||
name: 'Mattermost Desktop App',
|
||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.83 Electron/34.0.1 Safari/537.36 Mattermost/34.0.1',
|
||||
expectedBrowser: 'Mattermost Desktop App',
|
||||
expectedVersion: '34.0.1',
|
||||
},
|
||||
{
|
||||
name: 'Edge (Legacy)',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0) Edge/42.0',
|
||||
expectedBrowser: 'Edge',
|
||||
expectedVersion: '42',
|
||||
},
|
||||
{
|
||||
name: 'Edge Chromium',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0) Edg/92.0.234.1',
|
||||
expectedBrowser: 'Edge Chromium',
|
||||
expectedVersion: '92',
|
||||
},
|
||||
{
|
||||
name: 'Chrome',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0) Chrome/92.0.4515.131',
|
||||
expectedBrowser: 'Chrome',
|
||||
expectedVersion: '92',
|
||||
},
|
||||
{
|
||||
name: 'Opera',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0) Chrome/92.0.4515.131 OPR/77.0.4054.277',
|
||||
expectedBrowser: 'Opera',
|
||||
expectedVersion: '77',
|
||||
},
|
||||
{
|
||||
name: 'Safari',
|
||||
userAgent: 'Mozilla/5.0 (Macintosh) Version/14.1 Safari/605.1.15',
|
||||
expectedBrowser: 'Safari',
|
||||
expectedVersion: '14',
|
||||
},
|
||||
{
|
||||
name: 'Firefox',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0) Firefox/90.0',
|
||||
expectedBrowser: 'Firefox',
|
||||
expectedVersion: '90',
|
||||
},
|
||||
{
|
||||
name: 'Unknown Browser',
|
||||
userAgent: 'Some Unknown Browser',
|
||||
expectedBrowser: 'Unknown',
|
||||
expectedVersion: 'Unknown',
|
||||
},
|
||||
];
|
||||
|
||||
test.each(browserTestCases)(
|
||||
'should detect $name',
|
||||
({userAgent, expectedBrowser, expectedVersion}) => {
|
||||
// @ts-expect-error we can override the userAgent in tests
|
||||
window.navigator.userAgent = userAgent;
|
||||
const {browser, browserVersion} = getBrowserInfo();
|
||||
expect(browser).toBe(expectedBrowser);
|
||||
expect(browserVersion).toBe(expectedVersion);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('getPlatformInfo', () => {
|
||||
const platformTestCases = [
|
||||
{
|
||||
name: 'Windows using platform',
|
||||
platform: 'Win32',
|
||||
userAgent: '',
|
||||
expectedPlatform: 'Windows',
|
||||
},
|
||||
{
|
||||
name: 'MacOS using platform',
|
||||
platform: 'MacIntel',
|
||||
userAgent: '',
|
||||
expectedPlatform: 'MacOS',
|
||||
},
|
||||
{
|
||||
name: 'Linux using platform',
|
||||
platform: 'Linux x86_64',
|
||||
userAgent: '',
|
||||
expectedPlatform: 'Linux',
|
||||
},
|
||||
{
|
||||
name: 'Windows using userAgent',
|
||||
platform: '',
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0)',
|
||||
expectedPlatform: 'Windows',
|
||||
},
|
||||
{
|
||||
name: 'MacOS using userAgent',
|
||||
platform: '',
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
|
||||
expectedPlatform: 'MacOS',
|
||||
},
|
||||
{
|
||||
name: 'Linux using userAgent',
|
||||
platform: '',
|
||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64)',
|
||||
expectedPlatform: 'Linux',
|
||||
},
|
||||
{
|
||||
name: 'Unknown Platform',
|
||||
platform: '',
|
||||
userAgent: 'Some Unknown Platform',
|
||||
expectedPlatform: 'Unknown',
|
||||
},
|
||||
];
|
||||
|
||||
test.each(platformTestCases)(
|
||||
'should detect $name',
|
||||
({platform, userAgent, expectedPlatform}) => {
|
||||
// @ts-expect-error we can override the platform in tests
|
||||
window.navigator.platform = platform;
|
||||
|
||||
// @ts-expect-error we can override the userAgent in tests
|
||||
window.navigator.userAgent = userAgent;
|
||||
expect(getPlatformInfo()).toBe(expectedPlatform);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export function getBrowserInfo() {
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
|
||||
let browser = 'Unknown';
|
||||
let browserVersion = 'Unknown';
|
||||
|
||||
// Check if it's Mattermost Desktop App first
|
||||
if (userAgent.includes('mattermost')) {
|
||||
browser = 'Mattermost Desktop App';
|
||||
const match = userAgent.match(/mattermost\/(\d+(\.\d+)*)/i);
|
||||
|
||||
if (match && match[1]) {
|
||||
browserVersion = match[1];
|
||||
}
|
||||
return {browser, browserVersion};
|
||||
}
|
||||
|
||||
// Simple browser detection - order matters!
|
||||
if (userAgent.includes('edge/')) {
|
||||
browser = 'Edge';
|
||||
} else if (userAgent.includes('edg/')) {
|
||||
browser = 'Edge Chromium';
|
||||
} else if (userAgent.includes('chrome/')) {
|
||||
if (userAgent.includes('opr/')) {
|
||||
browser = 'Opera';
|
||||
} else {
|
||||
browser = 'Chrome';
|
||||
}
|
||||
} else if (userAgent.includes('safari/') && userAgent.includes('version/')) {
|
||||
browser = 'Safari';
|
||||
} else if (userAgent.includes('firefox/')) {
|
||||
browser = 'Firefox';
|
||||
}
|
||||
|
||||
// Get browser version
|
||||
let match;
|
||||
if (browser === 'Edge') {
|
||||
match = userAgent.match(/edge\/(\d+)/i);
|
||||
} else if (browser === 'Edge Chromium') {
|
||||
match = userAgent.match(/edg\/(\d+)/i);
|
||||
} else if (browser === 'Opera') {
|
||||
match = userAgent.match(/opr\/(\d+)/i);
|
||||
} else if (browser === 'Safari') {
|
||||
match = userAgent.match(/version\/(\d+)/i);
|
||||
} else {
|
||||
match = userAgent.match(/(firefox|chrome)\/(\d+)/i);
|
||||
if (match) {
|
||||
match[1] = match[2]; // Align with other matches where version is in group 1
|
||||
}
|
||||
}
|
||||
|
||||
if (match && match[1]) {
|
||||
browserVersion = match[1];
|
||||
}
|
||||
|
||||
return {browser, browserVersion};
|
||||
}
|
||||
|
||||
export function getPlatformInfo() {
|
||||
// Casting to undefined in case it is deprecated in any browser
|
||||
const platform = window.navigator.platform as string | undefined;
|
||||
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||
|
||||
let platformName = 'Unknown';
|
||||
|
||||
// First try using platform
|
||||
if (platform) {
|
||||
if (platform.toLowerCase().includes('win')) {
|
||||
platformName = 'Windows';
|
||||
} else if (platform.toLowerCase().includes('mac')) {
|
||||
platformName = 'MacOS';
|
||||
} else if (platform.toLowerCase().includes('linux')) {
|
||||
platformName = 'Linux';
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to userAgent if platform didn't work
|
||||
if (platformName === 'Unknown') {
|
||||
if (userAgent.includes('windows')) {
|
||||
platformName = 'Windows';
|
||||
} else if (userAgent.includes('mac os x')) {
|
||||
platformName = 'MacOS';
|
||||
} else if (userAgent.includes('linux')) {
|
||||
platformName = 'Linux';
|
||||
}
|
||||
}
|
||||
|
||||
return platformName;
|
||||
}
|
||||
@@ -168,6 +168,9 @@ export type ClientConfig = {
|
||||
PostEditTimeLimit: string;
|
||||
PrivacyPolicyLink: string;
|
||||
ReportAProblemLink: string;
|
||||
ReportAProblemType: string;
|
||||
ReportAProblemMail: string;
|
||||
AllowDownloadLogs: string;
|
||||
RequireEmailVerification: string;
|
||||
RestrictDirectMessage: string;
|
||||
RunJobs: string;
|
||||
@@ -627,6 +630,9 @@ export type SupportSettings = {
|
||||
AboutLink: string;
|
||||
HelpLink: string;
|
||||
ReportAProblemLink: string;
|
||||
ReportAProblemType: string;
|
||||
ReportAProblemMail: string;
|
||||
AllowDownloadLogs: boolean;
|
||||
ForgotPasswordLink: string;
|
||||
SupportEmail: string;
|
||||
CustomTermsOfServiceEnabled: boolean;
|
||||
|
||||
Ссылка в новой задаче
Block a user