mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-18 18:18:23 -05:00
[MM-67235] Add support for autotranslations on GM and DM (#35255)
* [MM-67235] Add support for autotranslations on GM and DM * Address copilot feedback * Fix tests * Fix bug and tests * Address feedback --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
8738f8c4b3
commit
a8dc8baa90
23 changed files with 1195 additions and 229 deletions
|
|
@ -162,6 +162,9 @@ describe('Channel Type Conversion (Public to Private Only)', () => {
|
|||
|
||||
// Verify settings were saved
|
||||
verifySettingsSaved();
|
||||
|
||||
// Verify the modal completely closed to avoid flakiness
|
||||
cy.get('#confirmModal').should('not.exist');
|
||||
};
|
||||
|
||||
// Function kept for potential future use but not used in current tests
|
||||
|
|
|
|||
|
|
@ -158,6 +158,9 @@ describe('Group Message Conversion To Private Channel', () => {
|
|||
// Open the GM
|
||||
cy.visit(`/${testTeam1.name}/messages/${gm.name}`);
|
||||
|
||||
// Wait until the channel is loaded
|
||||
cy.get('#channelHeaderDropdownButton').should('be.visible');
|
||||
|
||||
// convert via API call
|
||||
const timestamp = Date.now();
|
||||
cy.apiConvertGMToPrivateChannel(gm.id, testTeam2.id, `Channel ${timestamp}`, `c-${timestamp}`).then(() => {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
getMyCurrentChannelMembership,
|
||||
isCurrentChannelMuted,
|
||||
getCurrentChannelStats,
|
||||
getMyChannelAutotranslation,
|
||||
isMyChannelAutotranslated,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getConfig, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getRemoteNamesForChannel} from 'mattermost-redux/selectors/entities/shared_channels';
|
||||
|
|
@ -107,7 +107,7 @@ function makeMapStateToProps() {
|
|||
timestampUnits,
|
||||
hideGuestTags: config.HideGuestTags === 'true',
|
||||
sharedChannelsPluginsEnabled,
|
||||
isChannelAutotranslated: channel ? getMyChannelAutotranslation(state, channel.id) : false,
|
||||
isChannelAutotranslated: channel ? isMyChannelAutotranslated(state, channel.id) : false,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import ChevronDownIcon from '@mattermost/compass-icons/components/chevron-down';
|
|||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {
|
||||
getChannelAutotranslation,
|
||||
isChannelAutotranslated as isChannelAutotranslatedSelector,
|
||||
getCurrentChannel,
|
||||
isCurrentChannelDefault,
|
||||
isCurrentChannelFavorite,
|
||||
|
|
@ -60,7 +60,7 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
|
|||
const pluginMenuItems = useSelector(getChannelHeaderMenuPluginComponents);
|
||||
const isChannelBookmarksEnabled = useSelector(getIsChannelBookmarksEnabled);
|
||||
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channel?.id);
|
||||
const isChannelAutotranslated = useSelector((state: GlobalState) => (channel?.id ? getChannelAutotranslation(state, channel.id) : false));
|
||||
const isChannelAutotranslated = useSelector((state: GlobalState) => (channel?.id ? isChannelAutotranslatedSelector(state, channel.id) : false));
|
||||
|
||||
const isReadonly = false;
|
||||
|
||||
|
|
@ -157,6 +157,7 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
|
|||
isFavorite={isFavorite}
|
||||
isMobile={isMobile || false}
|
||||
isChannelBookmarksEnabled={isChannelBookmarksEnabled}
|
||||
isChannelAutotranslated={isChannelAutotranslated}
|
||||
/>
|
||||
)}
|
||||
{isGroup && (
|
||||
|
|
@ -168,6 +169,7 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
|
|||
isFavorite={isFavorite}
|
||||
isMobile={isMobile || false}
|
||||
isChannelBookmarksEnabled={isChannelBookmarksEnabled}
|
||||
isChannelAutotranslated={isChannelAutotranslated}
|
||||
/>
|
||||
)}
|
||||
{(!isDirect && !isGroup) && (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ChannelHeaderDirectMenu from './channel_header_direct_menu';
|
||||
|
||||
const DM_CHANNEL_ID = 'dm_channel_id';
|
||||
const CURRENT_USER_ID = 'user_id';
|
||||
|
||||
function getBaseState(overrides?: DeepPartial<GlobalState>): DeepPartial<GlobalState> {
|
||||
const channel = TestHelper.getChannelMock({id: DM_CHANNEL_ID, type: 'D'});
|
||||
const currentUser = TestHelper.getUserMock({id: CURRENT_USER_ID});
|
||||
return {
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {
|
||||
[DM_CHANNEL_ID]: channel,
|
||||
},
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
EnableAutoTranslation: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: CURRENT_USER_ID,
|
||||
profiles: {
|
||||
[CURRENT_USER_ID]: currentUser,
|
||||
},
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function getStateWithRestrictedDMAndGM(): DeepPartial<GlobalState> {
|
||||
const state = getBaseState();
|
||||
state!.entities!.general!.config!.RestrictDMAndGMAutotranslation = 'true';
|
||||
return state;
|
||||
}
|
||||
|
||||
describe('components/ChannelHeaderMenu/ChannelHeaderDirectMenu', () => {
|
||||
const channel = TestHelper.getChannelMock({id: DM_CHANNEL_ID, type: 'D'});
|
||||
const user = TestHelper.getUserMock({id: CURRENT_USER_ID});
|
||||
const defaultProps = {
|
||||
channel,
|
||||
user,
|
||||
isMuted: false,
|
||||
isMobile: false,
|
||||
isFavorite: false,
|
||||
pluginItems: [],
|
||||
isChannelBookmarksEnabled: false,
|
||||
isChannelAutotranslated: false,
|
||||
};
|
||||
|
||||
it('shows Channel Settings when RestrictDMAndGMAutotranslation is not enabled', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ChannelHeaderDirectMenu {...defaultProps}/>
|
||||
</WithTestMenuContext>,
|
||||
getBaseState(),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Channel Settings')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Edit Header')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Edit Header when RestrictDMAndGMAutotranslation is enabled', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ChannelHeaderDirectMenu {...defaultProps}/>
|
||||
</WithTestMenuContext>,
|
||||
getStateWithRestrictedDMAndGM(),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Edit Header')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Channel Settings')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Auto-translation menu when isChannelAutotranslated is true', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ChannelHeaderDirectMenu
|
||||
{...defaultProps}
|
||||
isChannelAutotranslated={true}
|
||||
/>
|
||||
</WithTestMenuContext>,
|
||||
getBaseState(),
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Auto-translation/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show Auto-translation menu when isChannelAutotranslated is false', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ChannelHeaderDirectMenu
|
||||
{...defaultProps}
|
||||
isChannelAutotranslated={false}
|
||||
/>
|
||||
</WithTestMenuContext>,
|
||||
getBaseState(),
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/Auto-translation/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import type {ReactNode} from 'react';
|
||||
import React from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {CogOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
|
@ -10,10 +11,16 @@ import type {UserProfile} from '@mattermost/types/users';
|
|||
|
||||
import {isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {canAccessChannelSettings} from 'selectors/views/channel_settings';
|
||||
|
||||
import ChannelMoveToSubMenu from 'components/channel_move_to_sub_menu';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import MenuItemAutotranslation from '../menu_items/autotranslation';
|
||||
import MenuItemChannelBookmarks from '../menu_items/channel_bookmarks_submenu';
|
||||
import MenuItemChannelSettings from '../menu_items/channel_settings_menu';
|
||||
import CloseMessage from '../menu_items/close_message';
|
||||
import EditConversationHeader from '../menu_items/edit_conversation_header';
|
||||
import MenuItemPluginItems from '../menu_items/plugins_submenu';
|
||||
|
|
@ -30,9 +37,12 @@ interface Props extends Menu.FirstMenuItemProps {
|
|||
isFavorite: boolean;
|
||||
pluginItems: ReactNode[];
|
||||
isChannelBookmarksEnabled: boolean;
|
||||
isChannelAutotranslated: boolean;
|
||||
}
|
||||
|
||||
const ChannelHeaderDirectMenu = ({channel, user, isMuted, isMobile, isFavorite, pluginItems, isChannelBookmarksEnabled, ...rest}: Props) => {
|
||||
const ChannelHeaderDirectMenu = ({channel, user, isMuted, isMobile, isFavorite, pluginItems, isChannelBookmarksEnabled, isChannelAutotranslated, ...rest}: Props) => {
|
||||
const canAccessChannelSettingsForChannel = useSelector((state: GlobalState) => canAccessChannelSettings(state, channel.id));
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItemToggleInfo
|
||||
|
|
@ -55,10 +65,21 @@ const ChannelHeaderDirectMenu = ({channel, user, isMuted, isMobile, isFavorite,
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
<EditConversationHeader
|
||||
leadingElement={<CogOutlineIcon size='18px'/>}
|
||||
channel={channel}
|
||||
/>
|
||||
{canAccessChannelSettingsForChannel ? (
|
||||
<MenuItemChannelSettings
|
||||
channel={channel}
|
||||
/>
|
||||
) : (
|
||||
<EditConversationHeader
|
||||
leadingElement={<CogOutlineIcon size='18px'/>}
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
{isChannelAutotranslated && (
|
||||
<MenuItemAutotranslation
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
<Menu.Separator/>
|
||||
{!isGuest(user.roles) && isChannelBookmarksEnabled && (
|
||||
<MenuItemChannelBookmarks
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ChannelHeaderGroupMenu from './channel_header_group_menu';
|
||||
|
||||
const GM_CHANNEL_ID = 'gm_channel_id';
|
||||
const CURRENT_USER_ID = 'user_id';
|
||||
|
||||
function getBaseState(): DeepPartial<GlobalState> {
|
||||
const channel = TestHelper.getChannelMock({
|
||||
id: GM_CHANNEL_ID,
|
||||
type: 'G' as const,
|
||||
group_constrained: false,
|
||||
delete_at: 0,
|
||||
});
|
||||
const currentUser = TestHelper.getUserMock({id: CURRENT_USER_ID, roles: 'system_user'});
|
||||
return {
|
||||
entities: {
|
||||
channels: {
|
||||
channels: {
|
||||
[GM_CHANNEL_ID]: channel,
|
||||
},
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
EnableAutoTranslation: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: CURRENT_USER_ID,
|
||||
profiles: {
|
||||
[CURRENT_USER_ID]: currentUser,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getStateWithRestrictedDMAndGM(): DeepPartial<GlobalState> {
|
||||
const state = getBaseState();
|
||||
return {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
general: {
|
||||
config: {RestrictDMAndGMAutotranslation: 'true'},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('components/ChannelHeaderMenu/ChannelHeaderGroupMenu', () => {
|
||||
const channel = TestHelper.getChannelMock({
|
||||
id: GM_CHANNEL_ID,
|
||||
type: 'G' as const,
|
||||
group_constrained: false,
|
||||
delete_at: 0,
|
||||
});
|
||||
const user = TestHelper.getUserMock({id: CURRENT_USER_ID, roles: 'system_user'});
|
||||
const defaultProps = {
|
||||
channel,
|
||||
user,
|
||||
isMuted: false,
|
||||
isMobile: false,
|
||||
isFavorite: false,
|
||||
pluginItems: [],
|
||||
isChannelBookmarksEnabled: false,
|
||||
isChannelAutotranslated: false,
|
||||
};
|
||||
|
||||
it('shows Channel Settings when RestrictDMAndGMAutotranslation is not enabled', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ChannelHeaderGroupMenu {...defaultProps}/>
|
||||
</WithTestMenuContext>,
|
||||
getBaseState(),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Channel Settings')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Edit Header')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Settings submenu when RestrictDMAndGMAutotranslation is enabled', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ChannelHeaderGroupMenu {...defaultProps}/>
|
||||
</WithTestMenuContext>,
|
||||
getStateWithRestrictedDMAndGM(),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Settings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Auto-translation menu when isChannelAutotranslated is true', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ChannelHeaderGroupMenu
|
||||
{...defaultProps}
|
||||
isChannelAutotranslated={true}
|
||||
/>
|
||||
</WithTestMenuContext>,
|
||||
getBaseState(),
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Auto-translation/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show Auto-translation menu when isChannelAutotranslated is false', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ChannelHeaderGroupMenu
|
||||
{...defaultProps}
|
||||
isChannelAutotranslated={false}
|
||||
/>
|
||||
</WithTestMenuContext>,
|
||||
getBaseState(),
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/Auto-translation/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show Channel Settings when the channel is archived', () => {
|
||||
const archivedChannel = TestHelper.getChannelMock({
|
||||
...channel,
|
||||
delete_at: 1234567890,
|
||||
});
|
||||
const archivedChannelState = getBaseState();
|
||||
archivedChannelState.entities!.channels!.channels![GM_CHANNEL_ID]!.delete_at = 1234567890;
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ChannelHeaderGroupMenu
|
||||
{...defaultProps}
|
||||
channel={archivedChannel}
|
||||
/>
|
||||
</WithTestMenuContext>,
|
||||
archivedChannelState,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Channel Settings')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show Settings submenu when the channel is archived', () => {
|
||||
const archivedChannel = TestHelper.getChannelMock({
|
||||
...channel,
|
||||
delete_at: 1234567890,
|
||||
});
|
||||
const archivedChannelState = getStateWithRestrictedDMAndGM();
|
||||
archivedChannelState.entities!.channels!.channels![GM_CHANNEL_ID] = {
|
||||
...archivedChannelState.entities!.channels!.channels![GM_CHANNEL_ID],
|
||||
delete_at: 1234567890,
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ChannelHeaderGroupMenu
|
||||
{...defaultProps}
|
||||
channel={archivedChannel}
|
||||
/>
|
||||
</WithTestMenuContext>,
|
||||
archivedChannelState,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Settings')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
import type {ReactNode} from 'react';
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
|
|
@ -15,11 +16,17 @@ import type {UserProfile} from '@mattermost/types/users';
|
|||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import {canAccessChannelSettings} from 'selectors/views/channel_settings';
|
||||
|
||||
import ChannelMoveToSubMenu from 'components/channel_move_to_sub_menu';
|
||||
import * as Menu from 'components/menu';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import MenuItemAutotranslation from '../menu_items/autotranslation';
|
||||
import MenuItemChannelBookmarks from '../menu_items/channel_bookmarks_submenu';
|
||||
import MenuItemChannelSettings from '../menu_items/channel_settings_menu';
|
||||
import CloseMessage from '../menu_items/close_message';
|
||||
import MenuItemConvertToPrivate from '../menu_items/convert_gm_to_private';
|
||||
import EditConversationHeader from '../menu_items/edit_conversation_header';
|
||||
|
|
@ -39,12 +46,14 @@ interface Props extends Menu.FirstMenuItemProps {
|
|||
isFavorite: boolean;
|
||||
pluginItems: ReactNode[];
|
||||
isChannelBookmarksEnabled: boolean;
|
||||
isChannelAutotranslated: boolean;
|
||||
}
|
||||
|
||||
const ChannelHeaderGroupMenu = ({channel, user, isMuted, isMobile, isFavorite, pluginItems, isChannelBookmarksEnabled, ...rest}: Props) => {
|
||||
const ChannelHeaderGroupMenu = ({channel, user, isMuted, isMobile, isFavorite, pluginItems, isChannelBookmarksEnabled, isChannelAutotranslated, ...rest}: Props) => {
|
||||
const isGroupConstrained = channel?.group_constrained === true;
|
||||
const isArchived = channel.delete_at !== 0;
|
||||
const {formatMessage} = useIntl();
|
||||
const canAccessChannelSettingsForChannel = useSelector((state: GlobalState) => canAccessChannelSettings(state, channel.id));
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -69,38 +78,62 @@ const ChannelHeaderGroupMenu = ({channel, user, isMuted, isMobile, isFavorite, p
|
|||
</>
|
||||
)}
|
||||
{!isArchived && (
|
||||
<MenuItemNotification
|
||||
user={user}
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
{(!isArchived && isGuest(user.roles)) && (
|
||||
<EditConversationHeader
|
||||
leadingElement={<CogOutlineIcon size='18px'/>}
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
{(!isArchived && !isGroupConstrained && !isGuest(user.roles)) && (
|
||||
<Menu.SubMenu
|
||||
id={'channelSettings'}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.settings'
|
||||
defaultMessage='Settings'
|
||||
<>
|
||||
{(
|
||||
<MenuItemNotification
|
||||
user={user}
|
||||
channel={channel}
|
||||
/>
|
||||
}
|
||||
leadingElement={<CogOutlineIcon size={18}/>}
|
||||
trailingElements={<ChevronRightIcon size={16}/>}
|
||||
menuId={'channelSettings-menu'}
|
||||
menuAriaLabel={formatMessage({id: 'channel_header.settings', defaultMessage: 'Settings'})}
|
||||
>
|
||||
<EditConversationHeader
|
||||
channel={channel}
|
||||
/>
|
||||
<MenuItemConvertToPrivate
|
||||
channel={channel}
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
)}
|
||||
{canAccessChannelSettingsForChannel ? (
|
||||
<>
|
||||
<MenuItemChannelSettings
|
||||
channel={channel}
|
||||
/>
|
||||
{(!isGroupConstrained && !isGuest(user.roles)) && (
|
||||
<MenuItemConvertToPrivate
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{(isGuest(user.roles)) && (
|
||||
<EditConversationHeader
|
||||
leadingElement={<CogOutlineIcon size='18px'/>}
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
{(!isGroupConstrained && !isGuest(user.roles)) && (
|
||||
<Menu.SubMenu
|
||||
id={'channelSettings'}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.settings'
|
||||
defaultMessage='Settings'
|
||||
/>
|
||||
}
|
||||
leadingElement={<CogOutlineIcon size={18}/>}
|
||||
trailingElements={<ChevronRightIcon size={16}/>}
|
||||
menuId={'channelSettings-menu'}
|
||||
menuAriaLabel={formatMessage({id: 'channel_header.settings', defaultMessage: 'Settings'})}
|
||||
>
|
||||
<EditConversationHeader
|
||||
channel={channel}
|
||||
/>
|
||||
<MenuItemConvertToPrivate
|
||||
channel={channel}
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isChannelAutotranslated && (
|
||||
<MenuItemAutotranslation
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
{!isArchived && !isGuest(user.roles) && isChannelBookmarksEnabled && (
|
||||
<MenuItemChannelBookmarks
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {TranslateIcon} from '@mattermost/compass-icons/components';
|
|||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {setMyChannelAutotranslation} from 'mattermost-redux/actions/channels';
|
||||
import {isChannelAutotranslated, isUserLanguageSupportedForAutotranslation} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {isMyChannelAutotranslated, isUserLanguageSupportedForAutotranslation} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ interface Props extends Menu.FirstMenuItemProps {
|
|||
const Autotranslation = ({channel, ...rest}: Props): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isAutotranslated = useSelector((state: GlobalState) => isChannelAutotranslated(state, channel.id));
|
||||
const isAutotranslated = useSelector((state: GlobalState) => isMyChannelAutotranslated(state, channel.id));
|
||||
const isLanguageSupported = useSelector(isUserLanguageSupportedForAutotranslation);
|
||||
|
||||
const handleAutotranslationToggle = useCallback(() => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type {Channel} from '@mattermost/types/channels';
|
|||
import type {ServerError} from '@mattermost/types/errors';
|
||||
|
||||
import {patchChannel} from 'mattermost-redux/actions/channels';
|
||||
import {getChannelAutotranslation} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {isChannelAutotranslated as isChannelAutotranslatedSelector} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import ColorInput from 'components/color_input';
|
||||
import type {TextboxElement} from 'components/textbox';
|
||||
|
|
@ -65,7 +65,7 @@ function ChannelSettingsConfigurationTab({
|
|||
|
||||
const initialBannerInfo = channel.banner_info || DEFAULT_CHANNEL_BANNER;
|
||||
|
||||
const initialIsChannelAutotranslated = useSelector((state: GlobalState) => getChannelAutotranslation(state, channel.id));
|
||||
const initialIsChannelAutotranslated = useSelector((state: GlobalState) => isChannelAutotranslatedSelector(state, channel.id));
|
||||
const [isChannelAutotranslated, setIsChannelAutotranslated] = useState(initialIsChannelAutotranslated);
|
||||
|
||||
const [formError, setFormError] = useState('');
|
||||
|
|
|
|||
|
|
@ -202,7 +202,6 @@ describe('ChannelSettingsInfoTab', () => {
|
|||
// Note: URL should remain unchanged when editing existing channels
|
||||
expect(patchChannel).toHaveBeenCalledWith('channel1', {
|
||||
display_name: 'Updated Channel Name',
|
||||
name: 'test-channel', // URL should remain unchanged when editing existing channels
|
||||
purpose: 'Updated purpose',
|
||||
header: 'Updated header',
|
||||
});
|
||||
|
|
@ -241,7 +240,6 @@ describe('ChannelSettingsInfoTab', () => {
|
|||
// Verify patchChannel was called with the trimmed values
|
||||
expect(patchChannel).toHaveBeenCalledWith('channel1', {
|
||||
display_name: 'Channel Name With Whitespace', // Whitespace should be trimmed
|
||||
name: 'test-channel', // URL should remain unchanged when editing existing channels
|
||||
purpose: 'Purpose with whitespace', // Whitespace should be trimmed
|
||||
header: 'Header with whitespace', // Whitespace should be trimmed
|
||||
});
|
||||
|
|
|
|||
|
|
@ -51,19 +51,36 @@ function ChannelSettingsInfoTab({
|
|||
const shouldShowPreviewPurpose = useSelector(showPreviewOnChannelSettingsPurposeModal);
|
||||
const shouldShowPreviewHeader = useSelector(showPreviewOnChannelSettingsHeaderModal);
|
||||
|
||||
const isPrivate = channel.type === Constants.PRIVATE_CHANNEL;
|
||||
const isDirect = channel.type === Constants.DM_CHANNEL;
|
||||
const isGroup = channel.type === Constants.GM_CHANNEL;
|
||||
const isDMorGroupChannel = isDirect || isGroup;
|
||||
|
||||
// Permissions for transforming channel type
|
||||
const canConvertToPrivate = useSelector((state: GlobalState) =>
|
||||
haveIChannelPermission(state, channel.team_id, channel.id, Permissions.CONVERT_PUBLIC_CHANNEL_TO_PRIVATE),
|
||||
);
|
||||
const canConvertToPublic = useSelector((state: GlobalState) =>
|
||||
haveIChannelPermission(state, channel.team_id, channel.id, Permissions.CONVERT_PRIVATE_CHANNEL_TO_PUBLIC),
|
||||
);
|
||||
const canConvertToPrivate = useSelector((state: GlobalState) => {
|
||||
if (isDMorGroupChannel) {
|
||||
// Group channels can be converted to private,
|
||||
// but that is handled in a different flow.
|
||||
return false;
|
||||
}
|
||||
return haveIChannelPermission(state, channel.team_id, channel.id, Permissions.CONVERT_PUBLIC_CHANNEL_TO_PRIVATE);
|
||||
});
|
||||
|
||||
const canConvertToPublic = useSelector((state: GlobalState) => {
|
||||
if (isDMorGroupChannel) {
|
||||
return false;
|
||||
}
|
||||
return haveIChannelPermission(state, channel.team_id, channel.id, Permissions.CONVERT_PRIVATE_CHANNEL_TO_PUBLIC);
|
||||
});
|
||||
|
||||
// Permissions for managing channel (name, header, purpose)
|
||||
const channelPropertiesPermission = channel.type === Constants.PRIVATE_CHANNEL ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES;
|
||||
const canManageChannelProperties = useSelector((state: GlobalState) =>
|
||||
haveIChannelPermission(state, channel.team_id, channel.id, channelPropertiesPermission),
|
||||
);
|
||||
const channelPropertiesPermission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES;
|
||||
const canManageChannelProperties = useSelector((state: GlobalState) => {
|
||||
if (isDMorGroupChannel) {
|
||||
return true;
|
||||
}
|
||||
return haveIChannelPermission(state, channel.team_id, channel.id, channelPropertiesPermission);
|
||||
});
|
||||
|
||||
// Constants
|
||||
const HEADER_MAX_LENGTH = 1024;
|
||||
|
|
@ -190,7 +207,7 @@ function ChannelSettingsInfoTab({
|
|||
}
|
||||
}, [formError, channelNameError, setFormError, formatMessage]);
|
||||
|
||||
const handleServerError = (err: ServerError) => {
|
||||
const handleServerError = useCallback((err: ServerError) => {
|
||||
const errorMsg = err.message || formatMessage({id: 'channel_settings.unknown_error', defaultMessage: 'Something went wrong.'});
|
||||
setFormError(errorMsg);
|
||||
setSaveChangesPanelState('error');
|
||||
|
|
@ -203,7 +220,7 @@ function ChannelSettingsInfoTab({
|
|||
)) {
|
||||
setUrlError(errorMsg); // Set the URL error to show in the URL input
|
||||
}
|
||||
};
|
||||
}, [formatMessage]);
|
||||
|
||||
// Validate & Save - using useCallback to ensure it has the latest state values
|
||||
const handleSave = useCallback(async (): Promise<boolean> => {
|
||||
|
|
@ -228,13 +245,24 @@ function ChannelSettingsInfoTab({
|
|||
}
|
||||
}
|
||||
|
||||
// Build updated channel object
|
||||
const updated: Partial<Channel> = {
|
||||
display_name: displayName.trim(),
|
||||
name: channelUrl.trim(),
|
||||
purpose: channelPurpose.trim(),
|
||||
header: channelHeader.trim(),
|
||||
};
|
||||
const updated: Partial<Channel> = {};
|
||||
if (!isDMorGroupChannel && displayName.trim() !== channel.display_name) {
|
||||
updated.display_name = displayName.trim();
|
||||
}
|
||||
if (!isDMorGroupChannel && channelUrl.trim() !== channel.name) {
|
||||
updated.name = channelUrl.trim();
|
||||
}
|
||||
if (!isDMorGroupChannel && channelPurpose.trim() !== channel.purpose) {
|
||||
updated.purpose = channelPurpose.trim();
|
||||
}
|
||||
if (channelHeader.trim() !== channel.header) {
|
||||
updated.header = channelHeader.trim();
|
||||
}
|
||||
|
||||
if (Object.keys(updated).length === 0) {
|
||||
// Return true if no changes were made
|
||||
return true;
|
||||
}
|
||||
|
||||
const {data, error} = await dispatch(patchChannel(channel.id, updated));
|
||||
if (error) {
|
||||
|
|
@ -244,12 +272,14 @@ function ChannelSettingsInfoTab({
|
|||
|
||||
// After every successful save, update local state to match the saved values
|
||||
// with this, we make sure that the unsavedChanges check will return false after saving
|
||||
setDisplayName(data?.display_name ?? updated.display_name ?? '');
|
||||
setChannelURL(data?.name ?? updated.name ?? '');
|
||||
setChannelPurpose(data?.purpose ?? updated.purpose ?? '');
|
||||
setChannelHeader(data?.header ?? updated.header ?? '');
|
||||
if (!isDMorGroupChannel) {
|
||||
setDisplayName(data?.display_name ?? updated.display_name ?? channel.display_name);
|
||||
setChannelURL(data?.name ?? updated.name ?? channel.name);
|
||||
setChannelPurpose(data?.purpose ?? updated.purpose ?? channel.purpose);
|
||||
}
|
||||
setChannelHeader(data?.header ?? updated.header ?? channel.header);
|
||||
return true;
|
||||
}, [channel, displayName, channelType, channelUrl, channelPurpose, channelHeader, dispatch, formatMessage, handleServerError]);
|
||||
}, [channel, displayName, channelType, isDMorGroupChannel, channelUrl, channelPurpose, channelHeader, dispatch, formatMessage, handleServerError]);
|
||||
|
||||
// Handle save changes panel actions
|
||||
const handleSaveChanges = useCallback(async () => {
|
||||
|
|
@ -312,16 +342,19 @@ function ChannelSettingsInfoTab({
|
|||
|
||||
// Memoize the calculation for whether to show the save changes panel
|
||||
const shouldShowPanel = useMemo(() => {
|
||||
const unsavedChanges = channel ? (
|
||||
displayName.trim() !== channel.display_name ||
|
||||
channelUrl.trim() !== channel.name ||
|
||||
channelPurpose.trim() !== channel.purpose ||
|
||||
channelHeader.trim() !== channel.header ||
|
||||
channelType !== channel.type
|
||||
) : false;
|
||||
let unsavedChanges = false;
|
||||
if (channel) {
|
||||
unsavedChanges = unsavedChanges || channelHeader.trim() !== channel.header;
|
||||
if (!isDMorGroupChannel) {
|
||||
unsavedChanges = unsavedChanges || displayName.trim() !== channel.display_name;
|
||||
unsavedChanges = unsavedChanges || channelUrl.trim() !== channel.name;
|
||||
unsavedChanges = unsavedChanges || channelPurpose.trim() !== channel.purpose;
|
||||
unsavedChanges = unsavedChanges || channelType !== channel.type;
|
||||
}
|
||||
}
|
||||
|
||||
return unsavedChanges || saveChangesPanelState === 'saved';
|
||||
}, [channel, displayName, channelUrl, channelPurpose, channelHeader, channelType, saveChangesPanelState]);
|
||||
}, [channel, isDMorGroupChannel, displayName, channelUrl, channelPurpose, channelHeader, channelType, saveChangesPanelState]);
|
||||
|
||||
return (
|
||||
<div className='ChannelSettingsModal__infoTab'>
|
||||
|
|
@ -348,74 +381,79 @@ function ChannelSettingsInfoTab({
|
|||
>
|
||||
{formatMessage({id: 'channel_settings.channel_info_tab.name', defaultMessage: 'Channel Info'})}
|
||||
</div>
|
||||
<ChannelNameFormField
|
||||
value={displayName}
|
||||
name='channel-settings-name'
|
||||
placeholder={formatMessage({
|
||||
id: 'channel_settings_modal.name.placeholder',
|
||||
defaultMessage: 'Enter a name for your channel',
|
||||
})}
|
||||
onDisplayNameChange={(name) => {
|
||||
setDisplayName(name);
|
||||
}}
|
||||
onURLChange={handleURLChange}
|
||||
onErrorStateChange={handleChannelNameError}
|
||||
urlError={internalUrlError}
|
||||
currentUrl={channelUrl}
|
||||
readOnly={!canManageChannelProperties}
|
||||
isEditingExistingChannel={true}
|
||||
/>
|
||||
|
||||
{!isDMorGroupChannel && (
|
||||
<ChannelNameFormField
|
||||
value={displayName}
|
||||
name='channel-settings-name'
|
||||
placeholder={formatMessage({
|
||||
id: 'channel_settings_modal.name.placeholder',
|
||||
defaultMessage: 'Enter a name for your channel',
|
||||
})}
|
||||
onDisplayNameChange={(name) => {
|
||||
setDisplayName(name);
|
||||
}}
|
||||
onURLChange={handleURLChange}
|
||||
onErrorStateChange={handleChannelNameError}
|
||||
urlError={internalUrlError}
|
||||
currentUrl={channelUrl}
|
||||
readOnly={!canManageChannelProperties}
|
||||
isEditingExistingChannel={true}
|
||||
/>
|
||||
)}
|
||||
{/* Channel Type Section*/}
|
||||
<PublicPrivateSelector
|
||||
className='ChannelSettingsModal__typeSelector'
|
||||
selected={channelType}
|
||||
publicButtonProps={{
|
||||
title: formatMessage({id: 'channel_modal.type.public.title', defaultMessage: 'Public Channel'}),
|
||||
description: formatMessage({id: 'channel_modal.type.public.description', defaultMessage: 'Anyone can join'}),
|
||||
{!isDMorGroupChannel && (
|
||||
<PublicPrivateSelector
|
||||
className='ChannelSettingsModal__typeSelector'
|
||||
selected={channelType}
|
||||
publicButtonProps={{
|
||||
title: formatMessage({id: 'channel_modal.type.public.title', defaultMessage: 'Public Channel'}),
|
||||
description: formatMessage({id: 'channel_modal.type.public.description', defaultMessage: 'Anyone can join'}),
|
||||
|
||||
// Always disable public button if current channel is private, regardless of permissions
|
||||
disabled: channel.type === Constants.PRIVATE_CHANNEL || !canConvertToPublic,
|
||||
}}
|
||||
privateButtonProps={{
|
||||
title: formatMessage({id: 'channel_modal.type.private.title', defaultMessage: 'Private Channel'}),
|
||||
description: formatMessage({id: 'channel_modal.type.private.description', defaultMessage: 'Only invited members'}),
|
||||
disabled: !canConvertToPrivate,
|
||||
}}
|
||||
onChange={handleChannelTypeChange}
|
||||
/>
|
||||
// Always disable public button if current channel is private, regardless of permissions
|
||||
disabled: channel.type === Constants.PRIVATE_CHANNEL || !canConvertToPublic,
|
||||
}}
|
||||
privateButtonProps={{
|
||||
title: formatMessage({id: 'channel_modal.type.private.title', defaultMessage: 'Private Channel'}),
|
||||
description: formatMessage({id: 'channel_modal.type.private.description', defaultMessage: 'Only invited members'}),
|
||||
disabled: !canConvertToPrivate,
|
||||
}}
|
||||
onChange={handleChannelTypeChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Purpose Section*/}
|
||||
<AdvancedTextbox
|
||||
id='channel_settings_purpose_textbox'
|
||||
value={channelPurpose}
|
||||
channelId={channel.id}
|
||||
onChange={handlePurposeChange}
|
||||
createMessage={formatMessage({
|
||||
id: 'channel_settings_modal.purpose.placeholder',
|
||||
defaultMessage: 'Enter a purpose for this channel (optional)',
|
||||
})}
|
||||
maxLength={Constants.MAX_CHANNELPURPOSE_LENGTH}
|
||||
preview={shouldShowPreviewPurpose}
|
||||
togglePreview={togglePurposePreview}
|
||||
useChannelMentions={false}
|
||||
onKeyPress={() => {}}
|
||||
descriptionMessage={formatMessage({
|
||||
id: 'channel_settings.purpose.description',
|
||||
defaultMessage: 'Describe how this channel should be used.',
|
||||
})}
|
||||
hasError={channelPurpose.length > Constants.MAX_CHANNELPURPOSE_LENGTH}
|
||||
errorMessage={channelPurpose.length > Constants.MAX_CHANNELPURPOSE_LENGTH ? formatMessage({
|
||||
id: 'channel_settings.error_purpose_length',
|
||||
defaultMessage: 'The text entered exceeds the character limit. The channel purpose is limited to {maxLength} characters.',
|
||||
}, {
|
||||
maxLength: Constants.MAX_CHANNELPURPOSE_LENGTH,
|
||||
}) : undefined
|
||||
}
|
||||
showCharacterCount={channelPurpose.length > Constants.MAX_CHANNELPURPOSE_LENGTH}
|
||||
readOnly={!canManageChannelProperties}
|
||||
name={formatMessage({id: 'channel_settings.purpose.label', defaultMessage: 'Channel Purpose'})}
|
||||
/>
|
||||
{!isDMorGroupChannel && (
|
||||
<AdvancedTextbox
|
||||
id='channel_settings_purpose_textbox'
|
||||
value={channelPurpose}
|
||||
channelId={channel.id}
|
||||
onChange={handlePurposeChange}
|
||||
createMessage={formatMessage({
|
||||
id: 'channel_settings_modal.purpose.placeholder',
|
||||
defaultMessage: 'Enter a purpose for this channel (optional)',
|
||||
})}
|
||||
maxLength={Constants.MAX_CHANNELPURPOSE_LENGTH}
|
||||
preview={shouldShowPreviewPurpose}
|
||||
togglePreview={togglePurposePreview}
|
||||
useChannelMentions={false}
|
||||
onKeyPress={() => {}}
|
||||
descriptionMessage={formatMessage({
|
||||
id: 'channel_settings.purpose.description',
|
||||
defaultMessage: 'Describe how this channel should be used.',
|
||||
})}
|
||||
hasError={channelPurpose.length > Constants.MAX_CHANNELPURPOSE_LENGTH}
|
||||
errorMessage={channelPurpose.length > Constants.MAX_CHANNELPURPOSE_LENGTH ? formatMessage({
|
||||
id: 'channel_settings.error_purpose_length',
|
||||
defaultMessage: 'The text entered exceeds the character limit. The channel purpose is limited to {maxLength} characters.',
|
||||
}, {
|
||||
maxLength: Constants.MAX_CHANNELPURPOSE_LENGTH,
|
||||
}) : undefined
|
||||
}
|
||||
showCharacterCount={channelPurpose.length > Constants.MAX_CHANNELPURPOSE_LENGTH}
|
||||
readOnly={!canManageChannelProperties}
|
||||
name={formatMessage({id: 'channel_settings.purpose.label', defaultMessage: 'Channel Purpose'})}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Channel Header Section*/}
|
||||
<AdvancedTextbox
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type {Post} from '@mattermost/types/posts';
|
|||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {General, Preferences as ReduxPreferences} from 'mattermost-redux/constants';
|
||||
import {getDirectTeammate, getMyChannelAutotranslation} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getDirectTeammate, isMyChannelAutotranslated} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUserLocale} from 'mattermost-redux/selectors/entities/i18n';
|
||||
import {getPost, makeGetCommentCountForPost, makeIsPostCommentMention, isPostAcknowledgementsEnabled, isPostPriorityEnabled, isPostFlagged} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
|
@ -85,7 +85,7 @@ function isConsecutivePost(state: GlobalState, ownProps: OwnProps, locale: strin
|
|||
consecutivePost = areConsecutivePostsBySameUser(post, previousPost);
|
||||
}
|
||||
|
||||
if (previousPost && post && consecutivePost && getMyChannelAutotranslation(state, post.channel_id)) {
|
||||
if (previousPost && post && consecutivePost && isMyChannelAutotranslated(state, post.channel_id)) {
|
||||
const translation = getPostTranslation(post, locale);
|
||||
const previousTranslation = getPostTranslation(previousPost, locale);
|
||||
if (translation?.state !== previousTranslation?.state) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import {connect} from 'react-redux';
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
|
||||
import {getChannel, getCurrentChannel, isChannelAutotranslated} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getChannel, getCurrentChannel, isMyChannelAutotranslated} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
||||
import {getSelectedPostId} from 'selectors/rhs';
|
||||
|
|
@ -22,7 +22,7 @@ function mapStateToProps(state: GlobalState) {
|
|||
return {
|
||||
channelDisplayName,
|
||||
originalPost,
|
||||
isChannelAutotranslated: isChannelAutotranslated(state, originalPost.channel_id),
|
||||
isChannelAutotranslated: isMyChannelAutotranslated(state, originalPost.channel_id),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type {Dispatch} from 'redux';
|
|||
|
||||
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {isChannelAutotranslated} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {isMyChannelAutotranslated} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getRecentPostsChunkInChannel, makeGetPostsChunkAroundPost, getUnreadPostsChunk, getPost, isPostsChunkIncludingUnreadsPosts, getLimitedViews} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {memoizeResult} from 'mattermost-redux/utils/helpers';
|
||||
import {makePreparePostIdsForPostList} from 'mattermost-redux/utils/post_list';
|
||||
|
|
@ -102,7 +102,7 @@ function makeMapStateToProps() {
|
|||
shouldStartFromBottomWhenUnread,
|
||||
isMobileView: getIsMobileView(state),
|
||||
hasInaccessiblePosts,
|
||||
isChannelAutotranslated: isChannelAutotranslated(state, channelId),
|
||||
isChannelAutotranslated: isMyChannelAutotranslated(state, channelId),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type {Dispatch} from 'redux';
|
|||
import type {PostPreviewMetadata} from '@mattermost/types/posts';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {isChannelAutotranslated, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {isMyChannelAutotranslated, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getPost, isPostPriorityEnabled} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
|
@ -63,7 +63,7 @@ function makeMapStateToProps() {
|
|||
isEmbedVisible: embedVisible,
|
||||
compactDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
||||
isPostPriorityEnabled: isPostPriorityEnabled(state),
|
||||
isChannelAutotranslated: isChannelAutotranslated(state, previewPost?.channel_id),
|
||||
isChannelAutotranslated: isMyChannelAutotranslated(state, previewPost?.channel_id),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {useSelector} from 'react-redux';
|
|||
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
|
||||
import {isChannelAutotranslated} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {isMyChannelAutotranslated} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import PostComponent from 'components/post';
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ type Props = {
|
|||
}
|
||||
|
||||
export default function PostSearchResultsItem(props: Props) {
|
||||
const autotranslated = useSelector((state: GlobalState) => isChannelAutotranslated(state, props.post.channel_id));
|
||||
const autotranslated = useSelector((state: GlobalState) => isMyChannelAutotranslated(state, props.post.channel_id));
|
||||
return (
|
||||
<div
|
||||
className='search-item__container'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {isChannelAutotranslated, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {isMyChannelAutotranslated, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getPost, isPostPriorityEnabled, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getThread} from 'mattermost-redux/selectors/entities/threads';
|
||||
|
|
@ -36,7 +36,7 @@ function makeMapStateToProps() {
|
|||
postsInThread: getPostsForThread(state, post.id),
|
||||
thread: getThread(state, threadId),
|
||||
isPostPriorityEnabled: isPostPriorityEnabled(state),
|
||||
isChannelAutotranslated: isChannelAutotranslated(state, post.channel_id),
|
||||
isChannelAutotranslated: isMyChannelAutotranslated(state, post.channel_id),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {connect} from 'react-redux';
|
|||
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
|
||||
import {getDirectTeammate, isChannelAutotranslated} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getDirectTeammate, isMyChannelAutotranslated} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
|
@ -55,7 +55,7 @@ function makeMapStateToProps() {
|
|||
lastPost,
|
||||
replyListIds,
|
||||
newMessagesSeparatorActions,
|
||||
isChannelAutotranslated: isChannelAutotranslated(state, channelId),
|
||||
isChannelAutotranslated: isMyChannelAutotranslated(state, channelId),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3364,3 +3364,484 @@ describe('hasAutotranslationBecomeEnabled', () => {
|
|||
expect(Selectors.hasAutotranslationBecomeEnabled(enabledMemberState as GlobalState, enabledChannel)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isChannelAutotranslated', () => {
|
||||
const publicChannelId = 'public_channel_id';
|
||||
const dmChannelId = 'dm_channel_id';
|
||||
const gmChannelId = 'gm_channel_id';
|
||||
|
||||
const publicChannel = {
|
||||
...TestHelper.fakeChannelWithId('team_id'),
|
||||
id: publicChannelId,
|
||||
type: General.OPEN_CHANNEL,
|
||||
autotranslation: true,
|
||||
};
|
||||
const dmChannel = {
|
||||
...TestHelper.fakeChannelWithId('team_id'),
|
||||
id: dmChannelId,
|
||||
type: General.DM_CHANNEL,
|
||||
autotranslation: true,
|
||||
};
|
||||
const gmChannel = {
|
||||
...TestHelper.fakeChannelWithId('team_id'),
|
||||
id: gmChannelId,
|
||||
type: General.GM_CHANNEL,
|
||||
autotranslation: true,
|
||||
};
|
||||
|
||||
it('returns false when channel does not exist', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: {EnableAutoTranslation: 'true'}},
|
||||
channels: {channels: {}},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isChannelAutotranslated(state as GlobalState, 'nonexistent')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when channel has no autotranslation', () => {
|
||||
const channelWithoutAutotranslation = {...publicChannel, autotranslation: false};
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: {EnableAutoTranslation: 'true'}},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: channelWithoutAutotranslation},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isChannelAutotranslated(state as GlobalState, publicChannelId)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when EnableAutoTranslation is not enabled in config', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: {EnableAutoTranslation: 'false'}},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: publicChannel},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isChannelAutotranslated(state as GlobalState, publicChannelId)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for public channel with autotranslation when EnableAutoTranslation is enabled', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: {EnableAutoTranslation: 'true'}},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: publicChannel},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isChannelAutotranslated(state as GlobalState, publicChannelId)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for DM channel with autotranslation when RestrictDMAndGMAutotranslation is not enabled', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableAutoTranslation: 'true',
|
||||
RestrictDMAndGMAutotranslation: 'false',
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
channels: {[dmChannelId]: dmChannel},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isChannelAutotranslated(state as GlobalState, dmChannelId)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for DM channel with autotranslation when RestrictDMAndGMAutotranslation is not set', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {EnableAutoTranslation: 'true'},
|
||||
},
|
||||
channels: {
|
||||
channels: {[dmChannelId]: dmChannel},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isChannelAutotranslated(state as GlobalState, dmChannelId)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for DM channel when RestrictDMAndGMAutotranslation is enabled', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableAutoTranslation: 'true',
|
||||
RestrictDMAndGMAutotranslation: 'true',
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
channels: {[dmChannelId]: dmChannel},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isChannelAutotranslated(state as GlobalState, dmChannelId)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for GM channel with autotranslation when RestrictDMAndGMAutotranslation is not enabled', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableAutoTranslation: 'true',
|
||||
RestrictDMAndGMAutotranslation: 'false',
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
channels: {[gmChannelId]: gmChannel},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isChannelAutotranslated(state as GlobalState, gmChannelId)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for GM channel when RestrictDMAndGMAutotranslation is enabled', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableAutoTranslation: 'true',
|
||||
RestrictDMAndGMAutotranslation: 'true',
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
channels: {[gmChannelId]: gmChannel},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isChannelAutotranslated(state as GlobalState, gmChannelId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMyChannelAutotranslated', () => {
|
||||
const publicChannelId = 'public_channel_id';
|
||||
const dmChannelId = 'dm_channel_id';
|
||||
const gmChannelId = 'gm_channel_id';
|
||||
const currentUserId = 'current_user_id';
|
||||
|
||||
const publicChannel = {
|
||||
...TestHelper.fakeChannelWithId('team_id'),
|
||||
id: publicChannelId,
|
||||
type: General.OPEN_CHANNEL,
|
||||
autotranslation: true,
|
||||
};
|
||||
const dmChannel = {
|
||||
...TestHelper.fakeChannelWithId('team_id'),
|
||||
id: dmChannelId,
|
||||
type: General.DM_CHANNEL,
|
||||
autotranslation: true,
|
||||
};
|
||||
const gmChannel = {
|
||||
...TestHelper.fakeChannelWithId('team_id'),
|
||||
id: gmChannelId,
|
||||
type: General.GM_CHANNEL,
|
||||
autotranslation: true,
|
||||
};
|
||||
|
||||
const currentUserWithLocale = (locale: string) => ({
|
||||
...TestHelper.fakeUserWithId(),
|
||||
id: currentUserId,
|
||||
locale,
|
||||
});
|
||||
|
||||
const baseConfig = {
|
||||
EnableAutoTranslation: 'true' as const,
|
||||
AutoTranslationLanguages: 'en,es,fr',
|
||||
RestrictDMAndGMAutotranslation: 'false' as const,
|
||||
};
|
||||
|
||||
it('returns false when channel does not exist', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: baseConfig},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('en')},
|
||||
},
|
||||
channels: {channels: {}, myMembers: {}},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, 'nonexistent')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when EnableAutoTranslation is not enabled in config', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: {...baseConfig, EnableAutoTranslation: 'false'}},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('en')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: publicChannel},
|
||||
myMembers: {[publicChannelId]: {channel_id: publicChannelId}},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, publicChannelId)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when channel has no autotranslation', () => {
|
||||
const channelWithoutAutotranslation = {...publicChannel, autotranslation: false};
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: baseConfig},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('en')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: channelWithoutAutotranslation},
|
||||
myMembers: {[publicChannelId]: {channel_id: publicChannelId}},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, publicChannelId)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when my channel member has autotranslation_disabled', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: baseConfig},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('en')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: publicChannel},
|
||||
myMembers: {
|
||||
[publicChannelId]: {channel_id: publicChannelId, autotranslation_disabled: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, publicChannelId)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when user locale is not in AutoTranslationLanguages', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: baseConfig},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('de')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: publicChannel},
|
||||
myMembers: {[publicChannelId]: {channel_id: publicChannelId}},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, publicChannelId)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when AutoTranslationLanguages is empty or not set', () => {
|
||||
const stateEmpty: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: {...baseConfig, AutoTranslationLanguages: ''}},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('en')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: publicChannel},
|
||||
myMembers: {[publicChannelId]: {channel_id: publicChannelId}},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(stateEmpty as GlobalState, publicChannelId)).toBe(false);
|
||||
|
||||
const stateMissing: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: {EnableAutoTranslation: 'true', RestrictDMAndGMAutotranslation: 'false'}},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('en')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: publicChannel},
|
||||
myMembers: {[publicChannelId]: {channel_id: publicChannelId}},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(stateMissing as GlobalState, publicChannelId)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for public channel when all conditions are met', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: baseConfig},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('en')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: publicChannel},
|
||||
myMembers: {[publicChannelId]: {channel_id: publicChannelId}},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, publicChannelId)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when my channel member has autotranslation_disabled false', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: baseConfig},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('es')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: publicChannel},
|
||||
myMembers: {
|
||||
[publicChannelId]: {channel_id: publicChannelId, autotranslation_disabled: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, publicChannelId)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when my channel member is missing (no autotranslation_disabled)', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: baseConfig},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('en')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[publicChannelId]: publicChannel},
|
||||
myMembers: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, publicChannelId)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for DM channel when RestrictDMAndGMAutotranslation is enabled', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
...baseConfig,
|
||||
RestrictDMAndGMAutotranslation: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('en')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[dmChannelId]: dmChannel},
|
||||
myMembers: {[dmChannelId]: {channel_id: dmChannelId}},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, dmChannelId)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for GM channel when RestrictDMAndGMAutotranslation is enabled', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
...baseConfig,
|
||||
RestrictDMAndGMAutotranslation: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('en')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[gmChannelId]: gmChannel},
|
||||
myMembers: {[gmChannelId]: {channel_id: gmChannelId}},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, gmChannelId)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for DM channel when RestrictDMAndGMAutotranslation is not enabled', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: baseConfig},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('fr')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[dmChannelId]: dmChannel},
|
||||
myMembers: {[dmChannelId]: {channel_id: dmChannelId}},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, dmChannelId)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for GM channel when RestrictDMAndGMAutotranslation is not enabled', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {config: baseConfig},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {[currentUserId]: currentUserWithLocale('en')},
|
||||
},
|
||||
channels: {
|
||||
channels: {[gmChannelId]: gmChannel},
|
||||
myMembers: {[gmChannelId]: {channel_id: gmChannelId}},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, gmChannelId)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isChannelAutotranslated', () => {
|
||||
it('returns same result as getMyChannelAutotranslation', () => {
|
||||
const channelId = 'channel_id';
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableAutoTranslation: 'true',
|
||||
AutoTranslationLanguages: 'en',
|
||||
RestrictDMAndGMAutotranslation: 'false',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'user_id',
|
||||
profiles: {
|
||||
user_id: {
|
||||
...TestHelper.fakeUserWithId(),
|
||||
id: 'user_id',
|
||||
locale: 'en',
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
channels: {
|
||||
[channelId]: {
|
||||
...TestHelper.fakeChannelWithId('team_id'),
|
||||
id: channelId,
|
||||
autotranslation: true,
|
||||
},
|
||||
},
|
||||
myMembers: {[channelId]: {channel_id: channelId}},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, channelId)).toBe(
|
||||
Selectors.isMyChannelAutotranslated(state as GlobalState, channelId),
|
||||
);
|
||||
expect(Selectors.isMyChannelAutotranslated(state as GlobalState, channelId)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1458,29 +1458,38 @@ export function getChannelBanner(state: GlobalState, channelId: string): Channel
|
|||
return channel ? channel.banner_info : undefined;
|
||||
}
|
||||
|
||||
export function getChannelAutotranslation(state: GlobalState, channelId: string): boolean {
|
||||
const channel = getChannel(state, channelId);
|
||||
const config = getConfig(state);
|
||||
|
||||
return Boolean(channel?.autotranslation && config?.EnableAutoTranslation === 'true');
|
||||
}
|
||||
|
||||
export function getMyChannelAutotranslation(state: GlobalState, channelId: string): boolean {
|
||||
const locale = getCurrentUserLocale(state);
|
||||
const channel = getChannel(state, channelId);
|
||||
const myChannelMember = getMyChannelMember(state, channelId);
|
||||
const config = getConfig(state);
|
||||
const targetLanguages = config?.AutoTranslationLanguages?.split(',');
|
||||
return Boolean(
|
||||
config?.EnableAutoTranslation === 'true' &&
|
||||
channel?.autotranslation &&
|
||||
!myChannelMember?.autotranslation_disabled &&
|
||||
targetLanguages?.includes(locale),
|
||||
);
|
||||
}
|
||||
|
||||
// isChannelAutotranslated returns whether the channel is autotranslated
|
||||
// in general terms in this server.
|
||||
export function isChannelAutotranslated(state: GlobalState, channelId: string): boolean {
|
||||
return getMyChannelAutotranslation(state, channelId);
|
||||
const channel = getChannel(state, channelId);
|
||||
const config = getConfig(state);
|
||||
|
||||
if (!channel?.autotranslation || config?.EnableAutoTranslation !== 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// When autotranslation is restricted on DMs and GMs, do not consider them autotranslated
|
||||
const isDMOrGM = channel.type === General.DM_CHANNEL || channel.type === General.GM_CHANNEL;
|
||||
if (isDMOrGM && config?.RestrictDMAndGMAutotranslation === 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// isMyChannelAutotranslated returns whether the current user is seeing the autotranslated
|
||||
// version of the channel.
|
||||
export function isMyChannelAutotranslated(state: GlobalState, channelId: string): boolean {
|
||||
if (!isChannelAutotranslated(state, channelId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isUserLanguageSupportedForAutotranslation(state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const myChannelMember = getMyChannelMember(state, channelId);
|
||||
return !myChannelMember?.autotranslation_disabled;
|
||||
}
|
||||
|
||||
export function isUserLanguageSupportedForAutotranslation(state: GlobalState): boolean {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
|
@ -12,58 +14,74 @@ describe('Selectors.Views.ChannelSettings', () => {
|
|||
const channelId = 'channel1';
|
||||
const defaultChannelId = 'default_channel';
|
||||
const privateChannelId = 'private_channel1';
|
||||
const dmChannelId = 'dm_channel1';
|
||||
const gmChannelId = 'gm_channel1';
|
||||
|
||||
// Create a more complete mock state
|
||||
const baseState = {
|
||||
entities: {
|
||||
channels: {
|
||||
function getBaseState(): GlobalState {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
channels: {
|
||||
[channelId]: {
|
||||
id: channelId,
|
||||
team_id: teamId,
|
||||
name: 'test-channel',
|
||||
type: 'O', // Constants.OPEN_CHANNEL
|
||||
},
|
||||
[defaultChannelId]: {
|
||||
id: defaultChannelId,
|
||||
team_id: teamId,
|
||||
name: 'town-square', // Constants.DEFAULT_CHANNEL
|
||||
type: 'O', // Constants.OPEN_CHANNEL
|
||||
},
|
||||
[privateChannelId]: {
|
||||
id: privateChannelId,
|
||||
team_id: teamId,
|
||||
name: 'private-channel',
|
||||
type: 'P', // Constants.PRIVATE_CHANNEL
|
||||
channels: {
|
||||
[channelId]: {
|
||||
id: channelId,
|
||||
team_id: teamId,
|
||||
name: 'test-channel',
|
||||
type: 'O', // Constants.OPEN_CHANNEL
|
||||
},
|
||||
[defaultChannelId]: {
|
||||
id: defaultChannelId,
|
||||
team_id: teamId,
|
||||
name: 'town-square', // Constants.DEFAULT_CHANNEL
|
||||
type: 'O', // Constants.OPEN_CHANNEL
|
||||
},
|
||||
[privateChannelId]: {
|
||||
id: privateChannelId,
|
||||
team_id: teamId,
|
||||
name: 'private-channel',
|
||||
type: 'P', // Constants.PRIVATE_CHANNEL
|
||||
},
|
||||
[dmChannelId]: {
|
||||
id: dmChannelId,
|
||||
team_id: teamId,
|
||||
name: 'user1__user2',
|
||||
type: 'D', // Constants.DM_CHANNEL
|
||||
},
|
||||
[gmChannelId]: {
|
||||
id: gmChannelId,
|
||||
team_id: teamId,
|
||||
name: 'group-channel',
|
||||
type: 'G', // Constants.GM_CHANNEL
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {},
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {
|
||||
id: 'current_user_id',
|
||||
roles: 'system_user',
|
||||
roles: {
|
||||
roles: {},
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {
|
||||
id: 'current_user_id',
|
||||
roles: 'system_user',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: teamId,
|
||||
teams: {
|
||||
[teamId]: {
|
||||
id: teamId,
|
||||
name: 'test-team',
|
||||
currentTeamId: teamId,
|
||||
teams: {
|
||||
[teamId]: {
|
||||
id: teamId,
|
||||
name: 'test-team',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
};
|
||||
return state as GlobalState;
|
||||
}
|
||||
|
||||
// Mock the dependencies directly
|
||||
beforeEach(() => {
|
||||
|
|
@ -86,7 +104,7 @@ describe('Selectors.Views.ChannelSettings', () => {
|
|||
};
|
||||
|
||||
it('should return false when channel does not exist', () => {
|
||||
const result = canAccessChannelSettings(baseState, 'nonexistent_channel');
|
||||
const result = canAccessChannelSettings(getBaseState(), 'nonexistent_channel');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -96,7 +114,7 @@ describe('Selectors.Views.ChannelSettings', () => {
|
|||
[Permissions.MANAGE_PUBLIC_CHANNEL_BANNER]: false,
|
||||
[Permissions.DELETE_PUBLIC_CHANNEL]: false,
|
||||
});
|
||||
const result = canAccessChannelSettings(baseState, channelId);
|
||||
const result = canAccessChannelSettings(getBaseState(), channelId);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -106,7 +124,7 @@ describe('Selectors.Views.ChannelSettings', () => {
|
|||
[Permissions.MANAGE_PRIVATE_CHANNEL_BANNER]: false,
|
||||
[Permissions.DELETE_PRIVATE_CHANNEL]: false,
|
||||
});
|
||||
const result = canAccessChannelSettings(baseState, privateChannelId);
|
||||
const result = canAccessChannelSettings(getBaseState(), privateChannelId);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -116,7 +134,7 @@ describe('Selectors.Views.ChannelSettings', () => {
|
|||
[Permissions.MANAGE_PUBLIC_CHANNEL_BANNER]: true,
|
||||
[Permissions.DELETE_PUBLIC_CHANNEL]: false,
|
||||
});
|
||||
const result = canAccessChannelSettings(baseState, channelId);
|
||||
const result = canAccessChannelSettings(getBaseState(), channelId);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -126,7 +144,7 @@ describe('Selectors.Views.ChannelSettings', () => {
|
|||
[Permissions.MANAGE_PUBLIC_CHANNEL_BANNER]: false,
|
||||
[Permissions.DELETE_PUBLIC_CHANNEL]: true,
|
||||
});
|
||||
const result = canAccessChannelSettings(baseState, channelId);
|
||||
const result = canAccessChannelSettings(getBaseState(), channelId);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -147,7 +165,53 @@ describe('Selectors.Views.ChannelSettings', () => {
|
|||
[Permissions.MANAGE_PUBLIC_CHANNEL_BANNER]: false,
|
||||
[Permissions.DELETE_PUBLIC_CHANNEL]: true, // This should be ignored for default channel
|
||||
});
|
||||
const result = canAccessChannelSettings(baseState, defaultChannelId);
|
||||
const result = canAccessChannelSettings(getBaseState(), defaultChannelId);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
describe('DM and GM channels with RestrictDMAndGMAutotranslation', () => {
|
||||
it('should return false when autotranslation is not enabled for DM', () => {
|
||||
const state = getBaseState();
|
||||
state.entities.general.config.EnableAutoTranslation = 'false';
|
||||
const result = canAccessChannelSettings(state, dmChannelId);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when autotranslation is not enabled for GM', () => {
|
||||
const state = getBaseState();
|
||||
state.entities.general.config.EnableAutoTranslation = 'false';
|
||||
const result = canAccessChannelSettings(state, gmChannelId);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for DM channel when RestrictDMAndGMAutotranslation is not enabled', () => {
|
||||
const state = getBaseState();
|
||||
state.entities.general.config.EnableAutoTranslation = 'true';
|
||||
const result = canAccessChannelSettings(state, dmChannelId);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for GM channel when RestrictDMAndGMAutotranslation is not enabled', () => {
|
||||
const state = getBaseState();
|
||||
state.entities.general.config.EnableAutoTranslation = 'true';
|
||||
const result = canAccessChannelSettings(state, gmChannelId);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for DM channel when RestrictDMAndGMAutotranslation is enabled', () => {
|
||||
const state = getBaseState();
|
||||
state.entities.general.config.EnableAutoTranslation = 'true';
|
||||
state.entities.general.config.RestrictDMAndGMAutotranslation = 'true';
|
||||
const result = canAccessChannelSettings(state, dmChannelId);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for GM channel when RestrictDMAndGMAutotranslation is enabled', () => {
|
||||
const state = getBaseState();
|
||||
state.entities.general.config.EnableAutoTranslation = 'true';
|
||||
state.entities.general.config.RestrictDMAndGMAutotranslation = 'true';
|
||||
const result = canAccessChannelSettings(state, gmChannelId);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
|
@ -19,12 +20,22 @@ export const canAccessChannelSettings = createSelector(
|
|||
(state: GlobalState) => state,
|
||||
(state: GlobalState) => state.entities.channels.channels,
|
||||
(state: GlobalState, channelId: string) => channelId,
|
||||
(state, channels, channelId) => {
|
||||
(state: GlobalState) => getConfig(state)?.RestrictDMAndGMAutotranslation === 'true',
|
||||
(state: GlobalState) => getConfig(state)?.EnableAutoTranslation === 'true',
|
||||
(state, channels, channelId, isDMAndGMAutotranslationRestricted, isAutoTranslationEnabled) => {
|
||||
const channel = channels[channelId];
|
||||
if (!channel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isDM = channel.type === Constants.DM_CHANNEL;
|
||||
const isGM = channel.type === Constants.GM_CHANNEL;
|
||||
|
||||
// For DM and GM: allow Channel Settings when "Restrict auto-translation on DM and GM" is not enabled
|
||||
if (isDM || isGM) {
|
||||
return isAutoTranslationEnabled && !isDMAndGMAutotranslationRestricted;
|
||||
}
|
||||
|
||||
const isPrivate = channel.type === Constants.PRIVATE_CHANNEL;
|
||||
const isDefaultChannel = channel.name === Constants.DEFAULT_CHANNEL;
|
||||
|
||||
|
|
@ -51,6 +62,15 @@ export const canAccessChannelSettings = createSelector(
|
|||
bannerPermission,
|
||||
);
|
||||
|
||||
// Configuration tab (translation) permissions
|
||||
const translationPermission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_AUTO_TRANSLATION : Permissions.MANAGE_PUBLIC_CHANNEL_AUTO_TRANSLATION;
|
||||
const hasTranslationPermission = haveIChannelPermission(
|
||||
state,
|
||||
teamId,
|
||||
channelId,
|
||||
translationPermission,
|
||||
);
|
||||
|
||||
// Archive tab permissions
|
||||
const archivePermission = isPrivate ? Permissions.DELETE_PRIVATE_CHANNEL : Permissions.DELETE_PUBLIC_CHANNEL;
|
||||
|
||||
|
|
@ -62,6 +82,6 @@ export const canAccessChannelSettings = createSelector(
|
|||
);
|
||||
|
||||
// User can access channel settings if they have permission for at least one tab
|
||||
return hasInfoPermission || hasBannerPermission || hasArchivePermission;
|
||||
return hasInfoPermission || hasBannerPermission || hasTranslationPermission || hasArchivePermission;
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue