== `. Use `&&` / `||` (and/or) for multiple conditions. Group conditions with `()`.'}
+ message={intl.formatMessage({
+ id: 'admin.access_control.cel.help_text',
+ defaultMessage: 'Write rules like `user.attributes.{lessThan}attribute{greaterThan} == {lessSign}value{greaterSign}`. Use `&&` / `||` (and/or) for multiple conditions. Group conditions with `()`.',
+ }, {
+ lessThan: '<',
+ greaterThan: '>',
+ lessSign: '<',
+ greaterSign: '>',
+ })}
onLearnMoreClick={() => setShowHelpModal(true)}
/>
diff --git a/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.tsx b/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.tsx
index cdd90794bda..dea613d2026 100644
--- a/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.tsx
+++ b/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.tsx
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import cloneDeep from 'lodash/cloneDeep';
-import React, {useState, useEffect} from 'react';
+import React, {useState, useEffect, useMemo} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {GenericModal} from '@mattermost/components';
@@ -96,6 +96,21 @@ function PolicyDetails({
const abacActions = useChannelAccessControlActions();
+ // Memoize the custom no options message to avoid recreating it on every render
+ const customNoPrivateChannelsMessage = useMemo(() => (
+
+ ), []);
+
// Check if there are any usable attributes for ABAC
const noUsableAttributes = attributesLoaded && !hasUsableAttributes(autocompleteResult, accessControlSettings.EnableUserManagedAttributes);
@@ -603,6 +618,7 @@ function PolicyDetails({
groupID={''}
alreadySelected={Object.values(channelChanges.added).map((channel) => channel.id)}
excludeTypes={['O', 'D', 'G']}
+ customNoOptionsMessage={customNoPrivateChannelsMessage}
excludeGroupConstrained={true}
/>
)}
diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx
index 772bac906d5..ae980302394 100644
--- a/webapp/channels/src/components/admin_console/admin_definition.tsx
+++ b/webapp/channels/src/components/admin_console/admin_definition.tsx
@@ -3006,7 +3006,7 @@ const AdminDefinition: AdminDefinitionType = {
sections: [
{
key: 'PostSettings.Threads',
- title: 'Threads',
+ title: defineMessage({id: 'admin.posts.sections.threads.title', defaultMessage: 'Threads'}),
description: defineMessage({id: 'admin.posts.sections.threads.description', defaultMessage: 'Configure threaded discussions and auto-follow defaults.'}),
settings: [
{
@@ -3066,7 +3066,7 @@ const AdminDefinition: AdminDefinitionType = {
},
{
key: 'PostSettings.Drafts',
- title: 'Drafts and Scheduled Posts',
+ title: defineMessage({id: 'admin.posts.sections.drafts.title', defaultMessage: 'Drafts and Scheduled Posts'}),
description: defineMessage({id: 'admin.posts.sections.drafts.description', defaultMessage: 'Control draft syncing and scheduled sending.'}),
settings: [
{
@@ -3089,7 +3089,7 @@ const AdminDefinition: AdminDefinitionType = {
},
{
key: 'PostSettings.Priority',
- title: 'Priority & Urgent Notifications',
+ title: defineMessage({id: 'admin.posts.sections.priority.title', defaultMessage: 'Priority & Urgent Notifications'}),
description: defineMessage({id: 'admin.posts.sections.priority.description', defaultMessage: 'Set message priority and repeating notifications for urgent delivery.'}),
settings: [
{
@@ -3223,8 +3223,8 @@ const AdminDefinition: AdminDefinitionType = {
},
{
key: 'PostSettings.BurnOnRead',
- title: 'Self-Deleting Messages',
- description: defineMessage({id: 'admin.posts.sections.burnOnRead.description', defaultMessage: 'Controls for messages that delete automatically a certain time after being sent or read.'}),
+ title: defineMessage({id: 'admin.posts.sections.burnOnRead.title', defaultMessage: 'Burn-on-Read Messages'}),
+ description: defineMessage({id: 'admin.posts.sections.burnOnRead.description', defaultMessage: 'Controls for messages that delete automatically a certain time after being read.'}),
license_sku: LicenseSkus.EnterpriseAdvanced,
component: LicensedSectionContainer,
componentProps: {
@@ -3333,7 +3333,7 @@ const AdminDefinition: AdminDefinitionType = {
},
{
key: 'PostSettings.Previews',
- title: 'Content & Previews',
+ title: defineMessage({id: 'admin.posts.sections.previews.title', defaultMessage: 'Content & Previews'}),
description: defineMessage({id: 'admin.posts.sections.previews.description', defaultMessage: 'Configure link previews and how advanced formatting renders.'}),
settings: [
{
@@ -3437,7 +3437,7 @@ const AdminDefinition: AdminDefinitionType = {
},
{
key: 'PostSettings.Performance',
- title: 'Performance & Limits',
+ title: defineMessage({id: 'admin.posts.sections.performance.title', defaultMessage: 'Performance & Limits'}),
description: defineMessage({id: 'admin.posts.sections.performance.description', defaultMessage: 'Configure limits that protect client performance and rendering.'}),
settings: [
{
@@ -5395,6 +5395,21 @@ const AdminDefinition: AdminDefinitionType = {
),
isHidden: it.licensedForFeature('Cloud'),
},
+ {
+ type: 'text',
+ key: 'ServiceSettings.DCRRedirectURIAllowlist',
+ multiple: true,
+ label: defineMessage({id: 'admin.oauth.dcrRedirectURIAllowlistTitle', defaultMessage: 'DCR Redirect URI Allowlist:'}),
+ help_text: defineMessage({id: 'admin.oauth.dcrRedirectURIAllowlistDesc', defaultMessage: 'When Dynamic Client Registration is enabled, optionally restrict which redirect URIs can be registered. Enter comma-separated glob patterns (e.g. https://*.example.com/**). If empty, all valid redirect URIs are allowed. Patterns support * (single path segment) and ** (multi-segment path).'}),
+ help_text_markdown: false,
+ placeholder: defineMessage({id: 'admin.oauth.dcrRedirectURIAllowlistPlaceholder', defaultMessage: 'E.g.: https://*.example.com/**, https://app.example.com/callback'}),
+ isDisabled: it.any(
+ it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.INTEGRATION_MANAGEMENT)),
+ it.stateIsFalse('ServiceSettings.EnableOAuthServiceProvider'),
+ it.stateIsFalse('ServiceSettings.EnableDynamicClientRegistration'),
+ ),
+ isHidden: it.licensedForFeature('Cloud'),
+ },
{
type: 'number',
key: 'ServiceSettings.OutgoingIntegrationRequestsTimeout',
diff --git a/webapp/channels/src/components/admin_console/admin_definition_dcr_allowlist.test.tsx b/webapp/channels/src/components/admin_console/admin_definition_dcr_allowlist.test.tsx
new file mode 100644
index 00000000000..840be4024f1
--- /dev/null
+++ b/webapp/channels/src/components/admin_console/admin_definition_dcr_allowlist.test.tsx
@@ -0,0 +1,52 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import AdminDefinition from './admin_definition';
+import type {AdminDefinitionSettingInput} from './types';
+
+describe('AdminDefinition - DCR Redirect URI Allowlist', () => {
+ const getIntegrationManagementSettings = () => {
+ const integrationsSection = AdminDefinition.integrations.subsections.integration_management;
+ const schema = integrationsSection?.schema;
+ return (schema && 'settings' in schema && schema.settings) ? schema.settings : [];
+ };
+
+ test('should include DCR Redirect URI Allowlist setting in integration management', () => {
+ const settings = getIntegrationManagementSettings();
+ const allowlistSetting = settings.find((s) => s.key === 'ServiceSettings.DCRRedirectURIAllowlist');
+
+ expect(allowlistSetting).toBeDefined();
+ expect(allowlistSetting?.type).toBe('text');
+ expect((allowlistSetting as AdminDefinitionSettingInput)?.multiple).toBe(true);
+ expect(allowlistSetting?.key).toBe('ServiceSettings.DCRRedirectURIAllowlist');
+ });
+
+ test('DCR allowlist setting should be placed after EnableDynamicClientRegistration', () => {
+ const settings = getIntegrationManagementSettings();
+ const dcrIndex = settings.findIndex((s) => s.key === 'ServiceSettings.EnableDynamicClientRegistration');
+ const allowlistIndex = settings.findIndex((s) => s.key === 'ServiceSettings.DCRRedirectURIAllowlist');
+
+ expect(dcrIndex).toBeGreaterThanOrEqual(0);
+ expect(allowlistIndex).toBe(dcrIndex + 1);
+ });
+
+ test('DCR allowlist setting should have correct disabled and hidden conditions', () => {
+ const settings = getIntegrationManagementSettings();
+ const allowlistSetting = settings.find((s) => s.key === 'ServiceSettings.DCRRedirectURIAllowlist');
+
+ expect(allowlistSetting?.isDisabled).toBeDefined();
+ expect(typeof allowlistSetting?.isDisabled).toBe('function');
+ expect(allowlistSetting?.isHidden).toBeDefined();
+ expect(typeof allowlistSetting?.isHidden).toBe('function');
+
+ const isDisabled = allowlistSetting?.isDisabled as ((config: object, state: Record
) => boolean);
+ expect(isDisabled({}, {
+ 'ServiceSettings.EnableOAuthServiceProvider': false,
+ 'ServiceSettings.EnableDynamicClientRegistration': true,
+ })).toBe(true);
+ expect(isDisabled({}, {
+ 'ServiceSettings.EnableOAuthServiceProvider': true,
+ 'ServiceSettings.EnableDynamicClientRegistration': false,
+ })).toBe(true);
+ });
+});
diff --git a/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.tsx b/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.tsx
index 1ce95877c39..16b0a5db2fc 100644
--- a/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.tsx
+++ b/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.tsx
@@ -3,7 +3,7 @@
import React, {useEffect, useState} from 'react';
import './custom_profile_attributes.scss';
-import {FormattedMessage} from 'react-intl';
+import {FormattedMessage, defineMessage} from 'react-intl';
import {useSelector} from 'react-redux';
import {Link} from 'react-router-dom';
@@ -135,12 +135,10 @@ const CustomProfileAttributes: React.FC = (props: Props): JSX.Element | n
- }
+ title={defineMessage({
+ id: 'admin.customProfileAttributes.title',
+ defaultMessage: 'Custom profile attributes sync',
+ })}
container={false}
subtitle={
{
defaultMessage='Sections'
/>
- {memoizedSections.map((section) => (
-
- ))}
+ {memoizedSections.map((section) => {
+ const title = section.sectionTitle || section.title;
+ return (
+
+ );
+ })}
);
};
diff --git a/webapp/channels/src/components/admin_console/licensed_section_container/licensed_section_container.tsx b/webapp/channels/src/components/admin_console/licensed_section_container/licensed_section_container.tsx
index 53688c069c9..e379a543bde 100644
--- a/webapp/channels/src/components/admin_console/licensed_section_container/licensed_section_container.tsx
+++ b/webapp/channels/src/components/admin_console/licensed_section_container/licensed_section_container.tsx
@@ -20,7 +20,7 @@ import type {GlobalState} from 'types/store';
type OwnProps = {
settingsList: React.ReactNode[];
requiredSku: LicenseSkus;
- sectionTitle?: string;
+ sectionTitle?: string | MessageDescriptor;
sectionDescription?: string | MessageDescriptor;
featureDiscoveryConfig: {
featureName: string;
diff --git a/webapp/channels/src/components/admin_console/localization/auto_translation.tsx b/webapp/channels/src/components/admin_console/localization/auto_translation.tsx
index df762c5120d..1057c034a65 100644
--- a/webapp/channels/src/components/admin_console/localization/auto_translation.tsx
+++ b/webapp/channels/src/components/admin_console/localization/auto_translation.tsx
@@ -18,6 +18,7 @@ import {
import TextSetting from 'components/admin_console/text_setting';
import useGetAgentsBridgeEnabled from 'components/common/hooks/useGetAgentsBridgeEnabled';
import Toggle from 'components/toggle';
+import BetaTag from 'components/widgets/tag/beta_tag';
import * as I18n from 'i18n/i18n.jsx';
@@ -26,9 +27,10 @@ import AutoTranslationInfo from './auto_translation_info';
import LibreTranslateSettings from './libreTranslate_settings';
import type {SystemConsoleCustomSettingsComponentProps} from '../schema_admin_settings';
-import './localization.scss';
import type {SearchableStrings} from '../types';
+import './localization.scss';
+
const locales = I18n.getAllLanguages();
const messages = defineMessages({
@@ -153,6 +155,9 @@ export default function AutoTranslation(props: SystemConsoleCustomSettingsCompon