From 95cabdfb3b1380b44ca18e7878244f99fb19d2e5 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 19 Aug 2026 17:58:18 -0300 Subject: [PATCH 1/2] Graduate theme and onboarding settings to Site Configuration > Customization (#38027) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [MM-57808][MM-57809][MM-57811] Graduate theme settings to Site Configuration > Customization Move ThemeSettings.EnableThemeSelection, ThemeSettings.AllowCustomThemes, and ThemeSettings.DefaultTheme out of System Console > Experimental > Features and into Site Configuration > Customization, alongside the other branding settings. The config keys, defaults, license gating, and the AllowCustomThemes dependency on EnableThemeSelection are unchanged; only the System Console page and the access classification move. The access tags become site_customization, so the settings are now governed by sysconsole_{read,write}_site_customization rather than sysconsole_{read,write}_experimental_features. i18n ids move from admin.experimental.* to the Customization page's admin.customization.* convention, and the documentation entries move from the experimental configuration settings page to the site configuration settings page. * [MM-57812][MM-57813] Graduate tutorial and onboarding settings to Site Configuration > Customization Move ServiceSettings.EnableTutorial and ServiceSettings.EnableOnboardingFlow out of System Console > Experimental > Features and into Site Configuration > Customization, next to the desktop app landing page setting that also governs a user's first-run experience. The config keys and defaults are unchanged; only the System Console page and the access classification move. The access tags become site_customization, so the settings are now governed by sysconsole_{read,write}_site_customization rather than sysconsole_{read,write}_experimental_features. i18n ids move from admin.experimental.* to the Customization page's admin.customization.* convention, and the documentation entries move from the experimental configuration settings page to the site configuration settings page. * [MM-57810] Surface ThemeSettings.AllowedThemes in Site Configuration > Customization ThemeSettings.AllowedThemes has always existed in the server config and has always been honoured by the theme picker, but it was never represented in the System Console schema — not under Experimental > Features and not anywhere else. Add it to Site Configuration > Customization alongside the theme settings it constrains. The server model is []string, so this uses the existing `type: 'text'` with `multiple: true` widget, the same one ServiceSettings.DCRRedirectURIAllowlist uses for its []string. The admin console joins the array with commas for display and splits it back into an array on save, which matches how the client config already serialises the value, so no behaviour or serialisation changes. The setting picks up an access tag of site_customization, where it previously had none, and the documentation entry moves from the self-hosted-only section of the experimental configuration settings page to the site configuration settings page. Also add a test asserting the graduated settings are present on Customization, absent from Experimental > Features, and that AllowedThemes round-trips as a string array. * Add runtime-effect tests for graduated theme and onboarding settings Expand coverage for the graduated Customization settings so that they are verified to affect their features, not just to save: - ThemeSettings.AllowedThemes: premade theme chooser only renders the allow-listed themes (theme enforcement on/off). - ServiceSettings.EnableOnboardingFlow: onboarding task list is gated on the config value. - ServiceSettings.EnableTutorial: the Channels tour tip is gated on the config value. - EnableTutorial/EnableOnboardingFlow (and the theme bools) round-trip their value through the admin console schema. Co-authored-by: Jesse Hallam --------- Co-authored-by: Cursor Agent Co-authored-by: mattermost-code Co-authored-by: Jesse Hallam --- .../experimental-configuration-settings.mdx | 100 ---------- .../configure/site-configuration-settings.mdx | 98 ++++++++++ server/public/model/config.go | 12 +- .../admin_console/admin_definition.tsx | 171 ++++++++++-------- .../admin_definition_theme_settings.test.tsx | 101 +++++++++++ .../onboarding_tasklist.test.tsx | 80 ++++++++ ...sidebar_channel_link_tutorial_tip.test.tsx | 108 +++++++++++ .../premade_theme_chooser.test.tsx | 61 +++++++ webapp/channels/src/i18n/en.json | 33 ++-- 9 files changed, 566 insertions(+), 198 deletions(-) create mode 100644 webapp/channels/src/components/admin_console/admin_definition_theme_settings.test.tsx create mode 100644 webapp/channels/src/components/onboarding_tasklist/onboarding_tasklist.test.tsx create mode 100644 webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link_tutorial_tip.test.tsx create mode 100644 webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.test.tsx diff --git a/docs/main/administration-guide/configure/experimental-configuration-settings.mdx b/docs/main/administration-guide/configure/experimental-configuration-settings.mdx index 1e95f75958f2..2113e32eb81c 100644 --- a/docs/main/administration-guide/configure/experimental-configuration-settings.mdx +++ b/docs/main/administration-guide/configure/experimental-configuration-settings.mdx @@ -206,89 +206,6 @@ Available on [Enterprise Advanced](https://mattermost.com/pricing/) from Matterm -### Enable theme selection - -**True**: Enables the **Display \> Theme** tab in **Settings** so users can select their theme. - -**False**: Users cannot select a different theme. The **Display \> Theme** tab is hidden in **Settings**. - - --- - - - - - -
This feature's config.json setting is "EnableThemeSelection": true with options true and false.
- -### Allow custom themes - -**True**: Enables the **Display \> Theme \> Custom Theme** section in **Settings**. - -**False**: Users cannot use a custom theme. The **Display \> Theme \> Custom Theme** section is hidden in **Settings**. - - --- - - - - - -
This feature's config.json setting is "AllowCustomThemes": true with options true and false.
- -### Default theme - -Set a default theme that applies to all new users on the system. - - --- - - - - - -
This feature's config.json setting is "DefaultTheme": "default" with options "default", "organization", "mattermostDark", and "windows10".
- -### Enable tutorial - -**True**: Users are prompted with a tutorial when they open Mattermost for the first time after account creation. - -**False**: The tutorial is disabled. Users are placed in Town Square when they open Mattermost for the first time after account creation. - - --- - - - - - -
This feature's config.json setting is "ServiceSettings.EnableTutorial": true with options true and false.
- -### Enable onboarding flow - -**True**: New Mattermost users are shown key tasks to complete as part of initial onboarding. - -**False**: User onboarding tasks are disabled. Users are placed in Town Square when they open Mattermost for the first time after account creation. - - --- - - - - - -
This feature's config.json setting is "ServiceSettings.EnableOnboardingFlow": true with options true and false.
- ### User's status and profile fetching poll interval This setting configures the number of milliseconds to wait between fetching user statuses and profiles periodically. Set to `0` to disable. @@ -544,23 +461,6 @@ Each configuration value below includes a JSON path to access the value programm -### Allowed themes - -This setting isn't available in the System Console and can only be set in `config.json`. - -Select the themes that can be chosen by users when `EnableThemeSelection` is set to `true`. - - --- - - - - - -
This feature's config.json setting is "AllowedThemes": [] with string array input consisting of the options "default", "organization", "mattermostDark", and "windows10", such as ["mattermostDark", "windows10"].
- ### File Location diff --git a/docs/main/administration-guide/configure/site-configuration-settings.mdx b/docs/main/administration-guide/configure/site-configuration-settings.mdx index 3df838f38de4..bccb14bb7f3f 100644 --- a/docs/main/administration-guide/configure/site-configuration-settings.mdx +++ b/docs/main/administration-guide/configure/site-configuration-settings.mdx @@ -109,6 +109,74 @@ Access the following configuration settings in the System Console by going to ** +### Enable theme selection + + + + ++++ + + + + + + +
  • true: (Default) Enables the Display > Theme tab in Settings so users can select their theme.
  • false: Users can't select a different theme. The Display > Theme tab is hidden in Settings.
  • System Config path: Site Configuration > Customization
  • config.json setting: ThemeSettings > EnableThemeSelection > true
  • Environment variable: MM_THEMESETTINGS_ENABLETHEMESELECTION
+ +### Allow custom themes + + + + ++++ + + + + + + +
  • true: (Default) Enables the Display > Theme > Custom Theme section in Settings.
  • false: Users can't use a custom theme. The Display > Theme > Custom Theme section is hidden in Settings.
  • System Config path: Site Configuration > Customization
  • config.json setting: ThemeSettings > AllowCustomThemes > true
  • Environment variable: MM_THEMESETTINGS_ALLOWCUSTOMTHEMES
+ +### Allowed themes + + + + ++++ + + + + + + +

Restrict the themes users can select in Display > Theme to a comma-separated list of theme names. Requires Enable theme selection to be set to true.

String array input consisting of the options denim, sapphire, quartz, indigo, and onyx. Default is an empty array, which allows all themes.

  • System Config path: Site Configuration > Customization
  • config.json setting: ThemeSettings > AllowedThemes > []
  • Environment variable: MM_THEMESETTINGS_ALLOWEDTHEMES
+ +### Default theme + + + + ++++ + + + + + + +

Set a default theme that applies to all new users on the system.

String input. One of denim, sapphire, quartz, indigo, or onyx. Default is default, which resolves to the Denim theme.

  • System Config path: Site Configuration > Customization
  • config.json setting: ThemeSettings > DefaultTheme > default
  • Environment variable: MM_THEMESETTINGS_DEFAULTTHEME
+ ### Enable Ask Community link @@ -364,6 +432,36 @@ This setting is applicable to self-hosted deployments only. +### Enable tutorial + +
++++ + + + + + + +
  • true: (Default) Users are prompted with a tutorial when they open Mattermost for the first time after account creation.
  • false: The tutorial is disabled. Users are placed in Town Square when they open Mattermost for the first time after account creation.
  • System Config path: Site Configuration > Customization
  • config.json setting: ServiceSettings > EnableTutorial > true
  • Environment variable: MM_SERVICESETTINGS_ENABLETUTORIAL
+ +### Enable onboarding + + ++++ + + + + + + +
  • true: (Default) New users are shown key tasks to complete as part of initial onboarding.
  • false: User onboarding tasks are disabled.
  • System Config path: Site Configuration > Customization
  • config.json setting: ServiceSettings > EnableOnboardingFlow > true
  • Environment variable: MM_SERVICESETTINGS_ENABLEONBOARDINGFLOW
+ ### Enable desktop app landing page diff --git a/server/public/model/config.go b/server/public/model/config.go index 7851daa30269..686336782d4d 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -445,8 +445,8 @@ type ServiceSettings struct { EnableUserStatuses *bool `access:"write_restrictable,cloud_restrictable"` ExperimentalEnableAuthenticationTransfer *bool `access:"experimental_features"` ClusterLogTimeoutMilliseconds *int `access:"write_restrictable,cloud_restrictable"` - EnableTutorial *bool `access:"experimental_features"` - EnableOnboardingFlow *bool `access:"experimental_features"` + EnableTutorial *bool `access:"site_customization"` + EnableOnboardingFlow *bool `access:"site_customization"` ExperimentalEnableDefaultChannelLeaveJoinMessages *bool `access:"experimental_features"` ExperimentalGroupUnreadChannels *string `access:"experimental_features"` EnableAPITeamDeletion *bool @@ -2499,10 +2499,10 @@ func (s *AnnouncementSettings) SetDefaults() { } type ThemeSettings struct { - EnableThemeSelection *bool `access:"experimental_features"` - DefaultTheme *string `access:"experimental_features"` - AllowCustomThemes *bool `access:"experimental_features"` - AllowedThemes []string + EnableThemeSelection *bool `access:"site_customization"` + DefaultTheme *string `access:"site_customization"` + AllowCustomThemes *bool `access:"site_customization"` + AllowedThemes []string `access:"site_customization"` } func (s *ThemeSettings) SetDefaults() { diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx index a5505a73c159..96fdbbc9f1a9 100644 --- a/webapp/channels/src/components/admin_console/admin_definition.tsx +++ b/webapp/channels/src/components/admin_console/admin_definition.tsx @@ -2629,6 +2629,84 @@ const AdminDefinition: AdminDefinitionType = { it.stateIsFalse('TeamSettings.EnableCustomBrand'), ), }, + { + type: 'bool', + key: 'ThemeSettings.EnableThemeSelection', + label: defineMessage({id: 'admin.customization.enableThemeSelectionTitle', defaultMessage: 'Enable Theme Selection:'}), + help_text: defineMessage({id: 'admin.customization.enableThemeSelectionDesc', defaultMessage: 'Enables the **Display > Theme** tab in Settings so users can select their theme.'}), + help_text_markdown: true, + isHidden: it.any( + it.not(it.licensed), + it.licensedForSku('starter'), + ), + isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)), + }, + { + type: 'bool', + key: 'ThemeSettings.AllowCustomThemes', + label: defineMessage({id: 'admin.customization.allowCustomThemesTitle', defaultMessage: 'Allow Custom Themes:'}), + help_text: defineMessage({id: 'admin.customization.allowCustomThemesDesc', defaultMessage: 'Enables the **Display > Theme > Custom Theme** section in Settings.'}), + help_text_markdown: true, + isHidden: it.any( + it.not(it.licensed), + it.licensedForSku('starter'), + ), + isDisabled: it.any( + it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)), + it.stateIsFalse('ThemeSettings.EnableThemeSelection'), + ), + }, + { + type: 'text', + key: 'ThemeSettings.AllowedThemes', + multiple: true, + label: defineMessage({id: 'admin.customization.allowedThemesTitle', defaultMessage: 'Allowed Themes:'}), + help_text: defineMessage({id: 'admin.customization.allowedThemesDesc', defaultMessage: 'Restrict the themes users can select in **Display > Theme** to a comma-separated list of theme names. Valid names are `denim`, `sapphire`, `quartz`, `indigo`, and `onyx`. Leave this field blank to allow all themes.'}), + help_text_markdown: true, + placeholder: defineMessage({id: 'admin.customization.allowedThemesPlaceholder', defaultMessage: 'E.g.: "denim,onyx"'}), + isHidden: it.any( + it.not(it.licensed), + it.licensedForSku('starter'), + ), + isDisabled: it.any( + it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)), + it.stateIsFalse('ThemeSettings.EnableThemeSelection'), + ), + }, + { + type: 'dropdown', + key: 'ThemeSettings.DefaultTheme', + label: defineMessage({id: 'admin.customization.defaultThemeTitle', defaultMessage: 'Default Theme:'}), + help_text: defineMessage({id: 'admin.customization.defaultThemeDesc', defaultMessage: 'Set a default theme that applies to all new users on the system.'}), + help_text_markdown: true, + options: [ + { + value: 'denim', + display_name: defineMessage({id: 'admin.customization.defaultTheme.options.denim', defaultMessage: 'Denim'}), + }, + { + value: 'sapphire', + display_name: defineMessage({id: 'admin.customization.defaultTheme.options.sapphire', defaultMessage: 'Sapphire'}), + }, + { + value: 'quartz', + display_name: defineMessage({id: 'admin.customization.defaultTheme.options.quartz', defaultMessage: 'Quartz'}), + }, + { + value: 'indigo', + display_name: defineMessage({id: 'admin.customization.defaultTheme.options.indigo', defaultMessage: 'Indigo'}), + }, + { + value: 'onyx', + display_name: defineMessage({id: 'admin.customization.defaultTheme.options.onyx', defaultMessage: 'Onyx'}), + }, + ], + isHidden: it.any( + it.not(it.licensed), + it.licensedForSku('starter'), + ), + isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)), + }, { type: 'bool', key: 'SupportSettings.EnableAskCommunityLink', @@ -2767,6 +2845,22 @@ const AdminDefinition: AdminDefinitionType = { isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)), isHidden: it.configIsTrue('ExperimentalSettings', 'RestrictSystemAdmin'), }, + { + type: 'bool', + key: 'ServiceSettings.EnableTutorial', + label: defineMessage({id: 'admin.customization.enableTutorialTitle', defaultMessage: 'Enable Tutorial:'}), + help_text: defineMessage({id: 'admin.customization.enableTutorialDesc', defaultMessage: 'When true, users are prompted with a tutorial when they open Mattermost for the first time after account creation. When false, the tutorial is disabled, and users are placed in Town Square when they open Mattermost for the first time after account creation.'}), + help_text_markdown: false, + isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)), + }, + { + type: 'bool', + key: 'ServiceSettings.EnableOnboardingFlow', + label: defineMessage({id: 'admin.customization.enableOnboardingFlowTitle', defaultMessage: 'Enable Onboarding:'}), + help_text: defineMessage({id: 'admin.customization.enableOnboardingFlowDesc', defaultMessage: 'When true, new users are shown steps to complete as part of an onboarding process'}), + help_text_markdown: false, + isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.CUSTOMIZATION)), + }, { type: 'bool', key: 'ServiceSettings.EnableDesktopLandingPage', @@ -6598,83 +6692,6 @@ const AdminDefinition: AdminDefinitionType = { help_text_markdown: false, isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)), }, - { - type: 'bool', - key: 'ThemeSettings.EnableThemeSelection', - label: defineMessage({id: 'admin.experimental.enableThemeSelection.title', defaultMessage: 'Enable Theme Selection:'}), - help_text: defineMessage({id: 'admin.experimental.enableThemeSelection.desc', defaultMessage: 'Enables the **Display > Theme** tab in Settings so users can select their theme.'}), - help_text_markdown: true, - isHidden: it.any( - it.not(it.licensed), - it.licensedForSku('starter'), - ), - isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)), - }, - { - type: 'bool', - key: 'ThemeSettings.AllowCustomThemes', - label: defineMessage({id: 'admin.experimental.allowCustomThemes.title', defaultMessage: 'Allow Custom Themes:'}), - help_text: defineMessage({id: 'admin.experimental.allowCustomThemes.desc', defaultMessage: 'Enables the **Display > Theme > Custom Theme** section in Settings.'}), - help_text_markdown: true, - isHidden: it.any( - it.not(it.licensed), - it.licensedForSku('starter'), - ), - isDisabled: it.any( - it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)), - it.stateIsFalse('ThemeSettings.EnableThemeSelection'), - ), - }, - { - type: 'dropdown', - key: 'ThemeSettings.DefaultTheme', - label: defineMessage({id: 'admin.experimental.defaultTheme.title', defaultMessage: 'Default Theme:'}), - help_text: defineMessage({id: 'admin.experimental.defaultTheme.desc', defaultMessage: 'Set a default theme that applies to all new users on the system.'}), - help_text_markdown: true, - options: [ - { - value: 'denim', - display_name: defineMessage({id: 'admin.experimental.defaultTheme.options.denim', defaultMessage: 'Denim'}), - }, - { - value: 'sapphire', - display_name: defineMessage({id: 'admin.experimental.defaultTheme.options.sapphire', defaultMessage: 'Sapphire'}), - }, - { - value: 'quartz', - display_name: defineMessage({id: 'admin.experimental.defaultTheme.options.quartz', defaultMessage: 'Quartz'}), - }, - { - value: 'indigo', - display_name: defineMessage({id: 'admin.experimental.defaultTheme.options.indigo', defaultMessage: 'Indigo'}), - }, - { - value: 'onyx', - display_name: defineMessage({id: 'admin.experimental.defaultTheme.options.onyx', defaultMessage: 'Onyx'}), - }, - ], - isHidden: it.any( - it.not(it.licensed), - it.licensedForSku('starter'), - ), - isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)), - }, - { - type: 'bool', - key: 'ServiceSettings.EnableTutorial', - label: defineMessage({id: 'admin.experimental.enableTutorial.title', defaultMessage: 'Enable Tutorial:'}), - help_text: defineMessage({id: 'admin.experimental.enableTutorial.desc', defaultMessage: 'When true, users are prompted with a tutorial when they open Mattermost for the first time after account creation. When false, the tutorial is disabled, and users are placed in Town Square when they open Mattermost for the first time after account creation.'}), - help_text_markdown: false, - isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)), - }, - { - type: 'bool', - key: 'ServiceSettings.EnableOnboardingFlow', - label: defineMessage({id: 'admin.experimental.enableOnboardingFlow.title', defaultMessage: 'Enable Onboarding:'}), - help_text: defineMessage({id: 'admin.experimental.enableOnboardingFlow.desc', defaultMessage: 'When true, new users are shown steps to complete as part of an onboarding process'}), - help_text_markdown: false, - isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.EXPERIMENTAL.FEATURES)), - }, { type: 'number', key: 'ExperimentalSettings.UsersStatusAndProfileFetchingPollIntervalMilliseconds', diff --git a/webapp/channels/src/components/admin_console/admin_definition_theme_settings.test.tsx b/webapp/channels/src/components/admin_console/admin_definition_theme_settings.test.tsx new file mode 100644 index 000000000000..e7cdf580c2e6 --- /dev/null +++ b/webapp/channels/src/components/admin_console/admin_definition_theme_settings.test.tsx @@ -0,0 +1,101 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {AdminConfig} from '@mattermost/types/config'; + +import AdminDefinition from './admin_definition'; +import SchemaAdminSettings, {getConfigFromState} from './schema_admin_settings'; +import type {AdminDefinitionSetting, AdminDefinitionSettingInput, AdminDefinitionSubSectionSchema} from './types'; + +const customizationSchema = AdminDefinition.site.subsections.customization.schema as AdminDefinitionSubSectionSchema; + +const customizationSettings = ('settings' in customizationSchema && customizationSchema.settings) ? customizationSchema.settings : []; + +const experimentalSchema = AdminDefinition.experimental.subsections.experimental_features.schema; +const experimentalSettings = (experimentalSchema && 'settings' in experimentalSchema && experimentalSchema.settings) ? experimentalSchema.settings : []; + +const findSetting = (settings: AdminDefinitionSetting[], key: string) => settings.find((setting) => setting.key === key); + +// getStateFromConfig is a static hoisted through injectIntl, so it isn't visible on the exported type. +const {getStateFromConfig} = SchemaAdminSettings as unknown as { + getStateFromConfig: (config: Partial, schema: AdminDefinitionSubSectionSchema) => Record; +}; + +describe('AdminDefinition - graduated theme and onboarding settings', () => { + const graduatedKeys = [ + 'ThemeSettings.EnableThemeSelection', + 'ThemeSettings.AllowCustomThemes', + 'ThemeSettings.AllowedThemes', + 'ThemeSettings.DefaultTheme', + 'ServiceSettings.EnableTutorial', + 'ServiceSettings.EnableOnboardingFlow', + ]; + + test.each(graduatedKeys)('%s is defined on Site Configuration > Customization', (key) => { + expect(findSetting(customizationSettings, key)).toBeDefined(); + }); + + test.each(graduatedKeys)('%s is no longer defined under Experimental > Features', (key) => { + expect(findSetting(experimentalSettings, key)).toBeUndefined(); + }); + + const boolGraduatedKeys = [ + 'ThemeSettings.EnableThemeSelection', + 'ThemeSettings.AllowCustomThemes', + 'ServiceSettings.EnableTutorial', + 'ServiceSettings.EnableOnboardingFlow', + ]; + + test.each(boolGraduatedKeys)('%s is a bool control that round-trips its value through the admin console', (key) => { + const setting = findSetting(customizationSettings, key) as AdminDefinitionSetting; + + expect(setting.type).toBe('bool'); + + const schema = {...customizationSchema, settings: [setting]} as AdminDefinitionSubSectionSchema; + const [section, field] = key.split('.') as [keyof AdminConfig, string]; + + const stateFalse = getStateFromConfig( + {[section]: {[field]: false}} as unknown as Partial, + schema, + ); + expect(stateFalse[key]).toBe(false); + + const stateTrue = getStateFromConfig( + {[section]: {[field]: true}} as unknown as Partial, + schema, + ); + expect(stateTrue[key]).toBe(true); + + // The value chosen in the console must be the value written back to config, not the base config value. + const config = getConfigFromState( + {[section]: {[field]: true}} as unknown as Partial, + stateFalse, + schema, + () => false, + ); + expect((config[section] as Record)[field]).toBe(false); + }); + + test('AllowedThemes round-trips through the admin console as a string array', () => { + const setting = findSetting(customizationSettings, 'ThemeSettings.AllowedThemes') as AdminDefinitionSettingInput; + + expect(setting.type).toBe('text'); + expect(setting.multiple).toBe(true); + + const schema = {...customizationSchema, settings: [setting]} as AdminDefinitionSubSectionSchema; + + const state = getStateFromConfig( + {ThemeSettings: {AllowedThemes: ['denim', 'onyx']}} as unknown as Partial, + schema, + ); + expect(state['ThemeSettings.AllowedThemes']).toEqual(['denim', 'onyx']); + + const config = getConfigFromState( + {ThemeSettings: {AllowedThemes: []}} as unknown as Partial, + state, + schema, + () => false, + ); + expect(config.ThemeSettings?.AllowedThemes).toEqual(['denim', 'onyx']); + }); +}); diff --git a/webapp/channels/src/components/onboarding_tasklist/onboarding_tasklist.test.tsx b/webapp/channels/src/components/onboarding_tasklist/onboarding_tasklist.test.tsx new file mode 100644 index 000000000000..580f2ae39e29 --- /dev/null +++ b/webapp/channels/src/components/onboarding_tasklist/onboarding_tasklist.test.tsx @@ -0,0 +1,80 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils'; + +import {OnboardingTaskCategory, OnboardingTaskList} from 'components/onboarding_tasks'; + +import {renderWithContext, screen} from 'tests/react_testing_utils'; + +import OnBoardingTaskList from './onboarding_tasklist'; + +jest.mock('mattermost-redux/actions/admin', () => ({ + ...jest.requireActual('mattermost-redux/actions/admin'), + getPrevTrialLicense: jest.fn(() => ({type: 'MOCK_GET_PREV_TRIAL_LICENSE'})), +})); + +// Isolate the ServiceSettings.EnableOnboardingFlow gate from the unrelated +// onboarding-eligibility logic: force the task list to be shown so that the +// config value is the only remaining variable. +jest.mock('selectors/onboarding', () => ({ + ...jest.requireActual('selectors/onboarding'), + getShowTaskListBool: () => [true, false], +})); + +describe('components/onboarding_tasklist - EnableOnboardingFlow effect', () => { + const currentUserId = 'current_user_id'; + + const stateWithOnboardingFlow = (enableOnboardingFlow?: string) => ({ + entities: { + users: { + currentUserId, + profiles: { + [currentUserId]: {id: currentUserId, roles: 'system_user'}, + }, + }, + general: { + config: enableOnboardingFlow === undefined ? {} : {EnableOnboardingFlow: enableOnboardingFlow}, + license: {IsLicensed: 'false', Cloud: 'false'}, + }, + admin: { + prevTrialLicense: {IsLicensed: 'false'}, + }, + cloud: { + subscription: {}, + }, + preferences: { + myPreferences: { + [getPreferenceKey(OnboardingTaskCategory, OnboardingTaskList.ONBOARDING_TASK_LIST_SHOW)]: { + category: OnboardingTaskCategory, + name: OnboardingTaskList.ONBOARDING_TASK_LIST_SHOW, + user_id: currentUserId, + value: 'true', + }, + }, + }, + }, + }); + + const buttonName = 'Start the onboarding process.'; + + test('renders the onboarding task list when EnableOnboardingFlow is true', () => { + renderWithContext(, stateWithOnboardingFlow('true')); + + expect(screen.getByLabelText(buttonName)).toBeInTheDocument(); + }); + + test('hides the onboarding task list when EnableOnboardingFlow is false', () => { + renderWithContext(, stateWithOnboardingFlow('false')); + + expect(screen.queryByLabelText(buttonName)).not.toBeInTheDocument(); + }); + + test('hides the onboarding task list when EnableOnboardingFlow is unset', () => { + renderWithContext(, stateWithOnboardingFlow()); + + expect(screen.queryByLabelText(buttonName)).not.toBeInTheDocument(); + }); +}); diff --git a/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link_tutorial_tip.test.tsx b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link_tutorial_tip.test.tsx new file mode 100644 index 000000000000..5cbcc184233d --- /dev/null +++ b/webapp/channels/src/components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link_tutorial_tip.test.tsx @@ -0,0 +1,108 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import type {ChannelType} from '@mattermost/types/channels'; + +import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils'; + +import { + GenericTaskSteps, + OnboardingTaskCategory, + OnboardingTasksName, +} from 'components/onboarding_tasks'; +import SidebarChannelLink from 'components/sidebar/sidebar_channel/sidebar_channel_link'; +import {OnboardingTourSteps, TutorialTourName} from 'components/tours'; + +import {renderWithContext, screen} from 'tests/react_testing_utils'; +import Constants from 'utils/constants'; + +jest.mock('@mattermost/shared/utils/user_agent', () => ({ + isDesktopApp: jest.fn(() => false), +})); + +// Stub the tour tip so the assertion targets the config-driven decision to render it, +// not the internals of the shared TourTip component. +jest.mock('components/tours/onboarding_tour', () => ({ + ...jest.requireActual('components/tours/onboarding_tour'), + ChannelsAndDirectMessagesTour: () =>
, +})); + +describe('components/sidebar/sidebar_channel_link - EnableTutorial effect', () => { + const currentUserId = 'current_user_id'; + + const townSquare = { + id: 'town_square_channel_id', + display_name: 'Town Square', + create_at: 0, + update_at: 0, + delete_at: 0, + team_id: 'team_id', + type: 'O' as ChannelType, + name: Constants.DEFAULT_CHANNEL, + header: '', + purpose: '', + last_post_at: 0, + last_root_post_at: 0, + creator_id: '', + scheme_id: '', + group_constrained: false, + }; + + const ownProps = { + channel: townSquare, + link: '/team/channels/town-square', + label: 'Town Square', + icon: null, + isSharedChannel: false, + }; + + // A state where the Channels tour tip is armed: the user is on the channels tour + // step and the onboarding task has been triggered. The only remaining gate is + // the ServiceSettings.EnableTutorial config value. + const armedTutorialState = (enableTutorial: string) => ({ + entities: { + users: { + currentUserId, + profiles: { + [currentUserId]: {id: currentUserId, roles: 'system_user'}, + }, + }, + general: { + config: { + EnableTutorial: enableTutorial, + EnableOnboardingFlow: 'true', + }, + }, + preferences: { + myPreferences: { + [getPreferenceKey(TutorialTourName.ONBOARDING_TUTORIAL_STEP, currentUserId)]: { + category: TutorialTourName.ONBOARDING_TUTORIAL_STEP, + name: currentUserId, + user_id: currentUserId, + value: String(OnboardingTourSteps.CHANNELS_AND_DIRECT_MESSAGES), + }, + [getPreferenceKey(OnboardingTaskCategory, OnboardingTasksName.CHANNELS_TOUR)]: { + category: OnboardingTaskCategory, + name: OnboardingTasksName.CHANNELS_TOUR, + user_id: currentUserId, + value: String(GenericTaskSteps.STARTED), + }, + }, + }, + }, + }); + + test('shows the Channels tour tip when EnableTutorial is true', () => { + renderWithContext(, armedTutorialState('true')); + + expect(screen.getByTestId('channels-tour-tip')).toBeInTheDocument(); + }); + + test('hides the Channels tour tip when EnableTutorial is false', () => { + renderWithContext(, armedTutorialState('false')); + + expect(screen.queryByTestId('channels-tour-tip')).not.toBeInTheDocument(); + }); +}); diff --git a/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.test.tsx b/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.test.tsx new file mode 100644 index 000000000000..38e5432c6332 --- /dev/null +++ b/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.test.tsx @@ -0,0 +1,61 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {Preferences} from 'mattermost-redux/constants'; + +import {renderWithContext, screen} from 'tests/react_testing_utils'; + +import PremadeThemeChooser from './index'; + +const allThemeKeys = Object.keys(Preferences.THEMES); + +const renderChooser = (allowedThemes?: string) => { + return renderWithContext( + , + { + entities: { + general: { + config: allowedThemes === undefined ? {} : {AllowedThemes: allowedThemes}, + }, + }, + }, + ); +}; + +describe('components/user_settings/display/premade_theme_chooser', () => { + test('renders every premade theme when ThemeSettings.AllowedThemes is unset', () => { + renderChooser(); + + expect(document.querySelectorAll('.premade-themes')).toHaveLength(allThemeKeys.length); + }); + + test('renders every premade theme when ThemeSettings.AllowedThemes is empty', () => { + renderChooser(''); + + expect(document.querySelectorAll('.premade-themes')).toHaveLength(allThemeKeys.length); + }); + + test('renders only the themes listed in ThemeSettings.AllowedThemes', () => { + renderChooser('denim,onyx'); + + expect(document.querySelectorAll('.premade-themes')).toHaveLength(2); + expect(screen.getByText('Denim')).toBeInTheDocument(); + expect(screen.getByText('Onyx')).toBeInTheDocument(); + expect(screen.queryByText('Sapphire')).not.toBeInTheDocument(); + expect(screen.queryByText('Quartz')).not.toBeInTheDocument(); + expect(screen.queryByText('Indigo')).not.toBeInTheDocument(); + }); + + test('enforces a single allowed theme', () => { + renderChooser('onyx'); + + expect(document.querySelectorAll('.premade-themes')).toHaveLength(1); + expect(screen.getByText('Onyx')).toBeInTheDocument(); + expect(screen.queryByText('Denim')).not.toBeInTheDocument(); + }); +}); diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index cefc8bf5ba49..90f5906a1ff4 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -977,6 +977,11 @@ "admin.contentFlagging.title": "Content Flagging", "admin.custom_terms_of_service_feature_discovery.copy": "Create your own terms of service that new users must accept before accessing your Mattermost instance on desktop, web, or mobile.", "admin.custom_terms_of_service_feature_discovery.title": "Create custom terms of service with Mattermost Enterprise", + "admin.customization.allowCustomThemesDesc": "Enables the **Display > Theme > Custom Theme** section in Settings.", + "admin.customization.allowCustomThemesTitle": "Allow Custom Themes:", + "admin.customization.allowedThemesDesc": "Restrict the themes users can select in **Display > Theme** to a comma-separated list of theme names. Valid names are `denim`, `sapphire`, `quartz`, `indigo`, and `onyx`. Leave this field blank to allow all themes.", + "admin.customization.allowedThemesPlaceholder": "E.g.: \"denim,onyx\"", + "admin.customization.allowedThemesTitle": "Allowed Themes:", "admin.customization.allowSyncedDrafts": "Enable server syncing of message drafts:", "admin.customization.allowSyncedDraftsDesc": "When enabled, users message drafts will sync with the server so they can be accessed from any device. Users may opt out of this behaviour in Account settings.", "admin.customization.androidAppDownloadLinkDesc": "Add a link to download the Android app. Users who access the site on a mobile web browser will be prompted with a page giving them the option to download the app. Leave this field blank to prevent the page from appearing.", @@ -997,6 +1002,13 @@ "admin.customization.customUrlSchemes": "Custom URL Schemes:", "admin.customization.customUrlSchemesDesc": "Allows message text to link if it begins with any of the comma-separated URL schemes listed. By default, the following schemes will create links: \"http\", \"https\", \"ftp\", \"tel\", and \"mailto\".", "admin.customization.customUrlSchemesPlaceholder": "E.g.: \"git,smtp\"", + "admin.customization.defaultTheme.options.denim": "Denim", + "admin.customization.defaultTheme.options.indigo": "Indigo", + "admin.customization.defaultTheme.options.onyx": "Onyx", + "admin.customization.defaultTheme.options.quartz": "Quartz", + "admin.customization.defaultTheme.options.sapphire": "Sapphire", + "admin.customization.defaultThemeDesc": "Set a default theme that applies to all new users on the system.", + "admin.customization.defaultThemeTitle": "Default Theme:", "admin.customization.enableCustomEmojiDesc": "Enable users to create custom emoji for use in messages. When enabled, custom emoji settings can be accessed in Channels through the emoji picker.", "admin.customization.enableCustomEmojiTitle": "Enable Custom Emoji:", "admin.customization.enableDesktopLandingPageDesc": "Whether or not to prompt a user to use the Desktop App when they first use Mattermost.", @@ -1011,10 +1023,16 @@ "admin.customization.enableLatexTitle": "Enable Latex Rendering:", "admin.customization.enableLinkPreviewsDesc": "Display a preview of website content, image links and YouTube links below the message when available. The server must be connected to the internet and have access through the firewall (if applicable) to the websites from which previews are expected. Users can disable these previews from Settings > Display > Website Link Previews.", "admin.customization.enableLinkPreviewsTitle": "Enable website link previews:", + "admin.customization.enableOnboardingFlowDesc": "When true, new users are shown steps to complete as part of an onboarding process", + "admin.customization.enableOnboardingFlowTitle": "Enable Onboarding:", "admin.customization.enablePermalinkPreviewsDesc": "When enabled, links to Mattermost messages will generate a preview for any users that have access to the original message. Please review our documentation for details.", "admin.customization.enablePermalinkPreviewsTitle": "Enable message link previews:", "admin.customization.enableSVGsDesc": "Enable previews for SVG file attachments and allow them to appear in messages.\n\nEnabling SVGs is not recommended in environments where not all users are trusted.", "admin.customization.enableSVGsTitle": "Enable SVGs:", + "admin.customization.enableThemeSelectionDesc": "Enables the **Display > Theme** tab in Settings so users can select their theme.", + "admin.customization.enableThemeSelectionTitle": "Enable Theme Selection:", + "admin.customization.enableTutorialDesc": "When true, users are prompted with a tutorial when they open Mattermost for the first time after account creation. When false, the tutorial is disabled, and users are placed in Town Square when they open Mattermost for the first time after account creation.", + "admin.customization.enableTutorialTitle": "Enable Tutorial:", "admin.customization.frameAncestorDesc": "Allows the Mattermost web client to be embedded in other websites. Enter a space-separated list of domains that are allowed to embed the Mattermost web client. Leave blank to disallow embedding.", "admin.customization.frameAncestorTitle": "Frame Ancestors:", "admin.customization.iosAppDownloadLinkDesc": "Add a link to download the iOS app. Users who access the site on a mobile web browser will be prompted with a page giving them the option to download the app. Leave this field blank to prevent the page from appearing.", @@ -1291,8 +1309,6 @@ "admin.environment.smtp.smtpUsername.title": "SMTP Server Username:", "admin.environment.smtp.testing": "Testing...", "admin.environment.webServer": "Web Server", - "admin.experimental.allowCustomThemes.desc": "Enables the **Display > Theme > Custom Theme** section in Settings.", - "admin.experimental.allowCustomThemes.title": "Allow Custom Themes:", "admin.experimental.allowedEmailDomain.desc": "(Optional) When set, users must have an email ending in this domain to move threads. Multiple domains can be specified by separating them with commas.", "admin.experimental.allowedEmailDomain.title": "Allowed Email Domain", "admin.experimental.collapsedThreads.always_on": "Always On", @@ -1301,13 +1317,6 @@ "admin.experimental.collapsedThreads.desc": "When enabled (default off), users have the option to enable Threaded Discussions in Account Settings. When enabled (default on), users see Threaded Discussions by default and have the option to disable it in Account Settings. When always on, users are required to use Threaded Discussions and cannot disable it.", "admin.experimental.collapsedThreads.off": "Disabled", "admin.experimental.collapsedThreads.title": "Threaded Discussions", - "admin.experimental.defaultTheme.desc": "Set a default theme that applies to all new users on the system.", - "admin.experimental.defaultTheme.options.denim": "Denim", - "admin.experimental.defaultTheme.options.indigo": "Indigo", - "admin.experimental.defaultTheme.options.onyx": "Onyx", - "admin.experimental.defaultTheme.options.quartz": "Quartz", - "admin.experimental.defaultTheme.options.sapphire": "Sapphire", - "admin.experimental.defaultTheme.title": "Default Theme:", "admin.experimental.delayChannelAutocomplete.desc": "When true, the autocomplete for channel links (such as ~town-square) will only trigger after typing a tilde followed by a couple letters. When false, the autocomplete will appear as soon as the user types a tilde.", "admin.experimental.delayChannelAutocomplete.title": "Delay Channel Autocomplete:", "admin.experimental.disableAppBar.desc": "When false, all integrations move from the channel header to the Apps Bar. Channel header plugin icons that haven't explicitly registered an Apps Bar icon will be moved to the Apps Bar which may result in rendering issues.", @@ -1322,12 +1331,6 @@ "admin.experimental.emailBatchingInterval.desc": "Specify the maximum frequency, in seconds, which the batching job checks for new notifications. Longer batching intervals will increase performance.", "admin.experimental.emailBatchingInterval.example": "E.g.: \"30\"", "admin.experimental.emailBatchingInterval.title": "Email Batching Interval:", - "admin.experimental.enableOnboardingFlow.desc": "When true, new users are shown steps to complete as part of an onboarding process", - "admin.experimental.enableOnboardingFlow.title": "Enable Onboarding:", - "admin.experimental.enableThemeSelection.desc": "Enables the **Display > Theme** tab in Settings so users can select their theme.", - "admin.experimental.enableThemeSelection.title": "Enable Theme Selection:", - "admin.experimental.enableTutorial.desc": "When true, users are prompted with a tutorial when they open Mattermost for the first time after account creation. When false, the tutorial is disabled, and users are placed in Town Square when they open Mattermost for the first time after account creation.", - "admin.experimental.enableTutorial.title": "Enable Tutorial:", "admin.experimental.enableUserDeactivation.desc": "When true, users may deactivate their own account from **Settings > Advanced**. If a user deactivates their own account, they will get an email notification confirming they were deactivated. When false, users may not deactivate their own account.", "admin.experimental.enableUserDeactivation.title": "Enable Account Deactivation:", "admin.experimental.enableWatermark.desc": "When true, authenticated mobile sessions will display a watermark overlay showing the username, domain, date (YYYY-MM-DD), and time (HH:mm) for data loss prevention (DLP) purposes.", From 260d0cda822beded4517a191bce5f6504fa3c3f1 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 19 Aug 2026 20:28:51 -0300 Subject: [PATCH 2/2] Trim whitespace when saving comma-separated System Console settings (#38042) --- .../schema_admin_settings.test.tsx | 65 ++++++++++++++++++- .../admin_console/schema_admin_settings.tsx | 6 ++ .../premade_theme_chooser/index.ts | 2 +- .../premade_theme_chooser.test.tsx | 23 +++++++ .../premade_theme_chooser.tsx | 4 +- 5 files changed, 96 insertions(+), 4 deletions(-) diff --git a/webapp/channels/src/components/admin_console/schema_admin_settings.test.tsx b/webapp/channels/src/components/admin_console/schema_admin_settings.test.tsx index 419e55da2fb4..5ab12b6b26d9 100644 --- a/webapp/channels/src/components/admin_console/schema_admin_settings.test.tsx +++ b/webapp/channels/src/components/admin_console/schema_admin_settings.test.tsx @@ -12,7 +12,7 @@ import {defaultIntl} from 'tests/helpers/intl-test-helper'; import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils'; import {it} from './admin_definition_helpers'; -import SchemaAdminSettings, {SchemaAdminSettings as SchemaAdminSettingsClass} from './schema_admin_settings'; +import SchemaAdminSettings, {SchemaAdminSettings as SchemaAdminSettingsClass, getConfigFromState} from './schema_admin_settings'; import type {ConsoleAccess, AdminDefinitionSubSectionSchema, AdminDefinitionSettingInput} from './types'; import ValidationResult from './validation'; @@ -1119,3 +1119,66 @@ describe('components/admin_console/SchemaAdminSettings', () => { }); }); }); + +describe('components/admin_console/SchemaAdminSettings/getConfigFromState', () => { + const buildSchema = (setting: AdminDefinitionSettingInput) => ({ + id: 'ServiceSettings', + name: 'Service Settings', + settings: [setting], + } as unknown as AdminDefinitionSubSectionSchema); + + const multipleSetting = { + type: 'text', + key: 'ServiceSettings.DCRRedirectURIAllowlist', + label: 'label', + multiple: true, + } as AdminDefinitionSettingInput; + + test('should trim whitespace from a multiple text setting', () => { + const config = getConfigFromState( + {}, + {'ServiceSettings.DCRRedirectURIAllowlist': ['https://one.example.com', ' https://two.example.com']}, + buildSchema(multipleSetting), + () => false, + ); + + expect(config.ServiceSettings?.DCRRedirectURIAllowlist).toEqual(['https://one.example.com', 'https://two.example.com']); + }); + + test('should drop empty entries from a multiple text setting', () => { + const config = getConfigFromState( + {}, + {'ServiceSettings.DCRRedirectURIAllowlist': ['https://one.example.com', '', ' ']}, + buildSchema(multipleSetting), + () => false, + ); + + expect(config.ServiceSettings?.DCRRedirectURIAllowlist).toEqual(['https://one.example.com']); + }); + + test('should save an empty array for a blank multiple text setting', () => { + const config = getConfigFromState( + {}, + {'ServiceSettings.DCRRedirectURIAllowlist': [' ']}, + buildSchema(multipleSetting), + () => false, + ); + + expect(config.ServiceSettings?.DCRRedirectURIAllowlist).toEqual([]); + }); + + test('should preserve whitespace in a text setting that is not multiple', () => { + const config = getConfigFromState( + {}, + {'ServiceSettings.SiteURL': ' https://example.com '}, + buildSchema({ + type: 'text', + key: 'ServiceSettings.SiteURL', + label: 'label', + } as AdminDefinitionSettingInput), + () => false, + ); + + expect(config.ServiceSettings?.SiteURL).toBe(' https://example.com '); + }); +}); diff --git a/webapp/channels/src/components/admin_console/schema_admin_settings.tsx b/webapp/channels/src/components/admin_console/schema_admin_settings.tsx index 171a14dbdd2c..bd8174895860 100644 --- a/webapp/channels/src/components/admin_console/schema_admin_settings.tsx +++ b/webapp/channels/src/components/admin_console/schema_admin_settings.tsx @@ -1563,6 +1563,12 @@ export const getSettingValue = ( return setting.dynamic_value(state[setting.key], config, state); } + // A multiple text setting is typed as a comma-separated list, so discard the whitespace and + // empty entries that separating the values leaves behind before saving. + if (setting.type === Constants.SettingsTypes.TYPE_TEXT && setting.multiple && Array.isArray(state[setting.key])) { + return state[setting.key].map((value: string) => value.trim()).filter((value: string) => value !== ''); + } + return state[setting.key]; }; diff --git a/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/index.ts b/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/index.ts index fed2d34661da..22f1f02b43f7 100644 --- a/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/index.ts +++ b/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/index.ts @@ -12,7 +12,7 @@ import PremadeThemeChooser from './premade_theme_chooser'; function mapStateToProps(state: GlobalState) { const config = getConfig(state); - const allowedThemes = (config.AllowedThemes && config.AllowedThemes.split(',')) || []; + const allowedThemes = config.AllowedThemes?.split(',').map((theme) => theme.trim()).filter((theme) => theme !== '') || []; return { allowedThemes, diff --git a/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.test.tsx b/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.test.tsx index 38e5432c6332..773a6382d23b 100644 --- a/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.test.tsx +++ b/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.test.tsx @@ -58,4 +58,27 @@ describe('components/user_settings/display/premade_theme_chooser', () => { expect(screen.getByText('Onyx')).toBeInTheDocument(); expect(screen.queryByText('Denim')).not.toBeInTheDocument(); }); + + test('ignores the whitespace surrounding the themes listed in ThemeSettings.AllowedThemes', () => { + renderChooser(' denim , onyx '); + + expect(document.querySelectorAll('.premade-themes')).toHaveLength(2); + expect(screen.getByText('Denim')).toBeInTheDocument(); + expect(screen.getByText('Onyx')).toBeInTheDocument(); + expect(screen.queryByText('Sapphire')).not.toBeInTheDocument(); + }); + + test('ignores the empty entries in ThemeSettings.AllowedThemes', () => { + renderChooser('onyx,,'); + + expect(document.querySelectorAll('.premade-themes')).toHaveLength(1); + expect(screen.getByText('Onyx')).toBeInTheDocument(); + expect(screen.queryByText('Denim')).not.toBeInTheDocument(); + }); + + test('renders every premade theme when ThemeSettings.AllowedThemes is only whitespace', () => { + renderChooser(' , '); + + expect(document.querySelectorAll('.premade-themes')).toHaveLength(allThemeKeys.length); + }); }); diff --git a/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.tsx b/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.tsx index e90c0ad06449..51a6c28daa8e 100644 --- a/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.tsx +++ b/webapp/channels/src/components/user_settings/display/user_settings_theme/premade_theme_chooser/premade_theme_chooser.tsx @@ -19,11 +19,11 @@ type Props = { const PremadeThemeChooser = ({theme, updateTheme, allowedThemes = []}: Props) => { const premadeThemes = []; - const hasAllowedThemes = allowedThemes.length > 1 || (allowedThemes[0] && allowedThemes[0].trim().length > 0); + const hasAllowedThemes = allowedThemes.length > 0; for (const k in Preferences.THEMES) { if (Object.hasOwn(Preferences.THEMES, k)) { - if (hasAllowedThemes && allowedThemes.indexOf(k) < 0) { + if (hasAllowedThemes && !allowedThemes.includes(k)) { continue; }