Adds a feature flag and the logic to hide plugin interactions on shared channels (#31185)
* Adds a feature flag and the logic to hide plugin interactions on shared channels * Address review comments * Fix CI * Address review comments * Address review comments --------- Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es> Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
58d6c71ed2
Коммит
c6c27e7752
@@ -22,6 +22,9 @@ type FeatureFlags struct {
|
||||
// Enable DMs and GMs for shared channels.
|
||||
EnableSharedChannelsDMs bool
|
||||
|
||||
// Enable plugins in shared channels.
|
||||
EnableSharedChannelsPlugins bool
|
||||
|
||||
// AppsEnabled toggles the Apps framework functionalities both in server and client side
|
||||
AppsEnabled bool
|
||||
|
||||
@@ -66,6 +69,7 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.TestBoolFeature = false
|
||||
f.EnableRemoteClusterService = false
|
||||
f.EnableSharedChannelsDMs = false
|
||||
f.EnableSharedChannelsPlugins = true
|
||||
f.AppsEnabled = false
|
||||
f.NormalizeLdapDNs = false
|
||||
f.DeprecateCloudFree = false
|
||||
|
||||
@@ -298,7 +298,7 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}) || [];
|
||||
});
|
||||
|
||||
let appBindings = [] as JSX.Element[];
|
||||
if (this.props.appsEnabled && this.state.appBindings) {
|
||||
|
||||
@@ -12,7 +12,8 @@ import type {Post} from '@mattermost/types/posts';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {AppBindingLocations} from 'mattermost-redux/constants/apps';
|
||||
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
|
||||
import {isMarketplaceEnabled} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {isMarketplaceEnabled, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
@@ -47,6 +48,8 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
|
||||
const {post} = ownProps;
|
||||
|
||||
const systemMessage = isSystemMessage(post);
|
||||
const channel = getChannel(state, post.channel_id);
|
||||
const sharedChannelsPluginsEnabled = getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true';
|
||||
|
||||
const apps = appsEnabled(state);
|
||||
const showBindings = apps && !systemMessage && !isCombinedUserActivityPost(post.id);
|
||||
@@ -57,12 +60,14 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
|
||||
const currentUser = getCurrentUser(state);
|
||||
const isSysAdmin = isSystemAdmin(currentUser.roles);
|
||||
|
||||
const pluginItemsVisible = !channel?.shared || sharedChannelsPluginsEnabled;
|
||||
|
||||
return {
|
||||
appBindings,
|
||||
appsEnabled: apps,
|
||||
pluginMenuItemComponents: state.plugins.components.PostDropdownMenuItem,
|
||||
pluginMenuItemComponents: pluginItemsVisible ? state.plugins.components.PostDropdownMenuItem : [],
|
||||
isSysAdmin,
|
||||
pluginMenuItems: state.plugins.components.PostDropdownMenu,
|
||||
pluginMenuItems: pluginItemsVisible ? state.plugins.components.PostDropdownMenu : [],
|
||||
teamId: getCurrentTeamId(state),
|
||||
isMobileView: getIsMobileView(state),
|
||||
canOpenMarketplace: (
|
||||
|
||||
@@ -307,7 +307,7 @@ const AdvancedTextEditor = ({
|
||||
}, [dispatch, currentUserId, getFormattingBarPreferenceName, isFormattingBarHidden]);
|
||||
|
||||
useOrientationHandler(textboxRef, rootId);
|
||||
const pluginItems = usePluginItems(draft, textboxRef, handleDraftChange);
|
||||
const pluginItems = usePluginItems(draft, textboxRef, handleDraftChange, channelId);
|
||||
const focusTextbox = useTextboxFocus(textboxRef, channelId, isRHS, canPost);
|
||||
const [attachmentPreview, fileUploadJSX] = useUploadFiles(
|
||||
draft,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||
import type TextboxClass from 'components/textbox/textbox';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
@@ -13,8 +14,10 @@ const usePluginItems = (
|
||||
draft: PostDraft,
|
||||
textboxRef: React.RefObject<TextboxClass>,
|
||||
handleDraftChange: (draft: PostDraft) => void,
|
||||
channelId?: string,
|
||||
) => {
|
||||
const postEditorActions = useSelector((state: GlobalState) => state.plugins.components.PostEditorAction);
|
||||
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channelId);
|
||||
|
||||
const getSelectedText = useCallback(() => {
|
||||
const input = textboxRef.current?.getInputBox();
|
||||
@@ -34,21 +37,27 @@ const usePluginItems = (
|
||||
// Missing setting the state eventually?
|
||||
}, [handleDraftChange, draft]);
|
||||
|
||||
const items = useMemo(() => postEditorActions?.map((item) => {
|
||||
if (!item.component) {
|
||||
return null;
|
||||
const items = useMemo(() => {
|
||||
if (!pluginItemsVisible) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const Component = item.component as any;
|
||||
return (
|
||||
<Component
|
||||
key={item.id}
|
||||
draft={draft}
|
||||
getSelectedText={getSelectedText}
|
||||
updateText={updateText}
|
||||
/>
|
||||
);
|
||||
}), [postEditorActions, draft, getSelectedText, updateText]);
|
||||
return postEditorActions?.map((item) => {
|
||||
if (!item.component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Component = item.component as any;
|
||||
return (
|
||||
<Component
|
||||
key={item.id}
|
||||
draft={draft}
|
||||
getSelectedText={getSelectedText}
|
||||
updateText={updateText}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}, [postEditorActions, draft, getSelectedText, updateText, pluginItemsVisible]);
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -2700,53 +2700,6 @@ exports[`components/ChannelHeader should render shared view 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Connect(injectIntl(ChannelHeaderPlug))
|
||||
channel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "Test",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"shared": true,
|
||||
"team_id": "team_id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
channelMember={
|
||||
Object {
|
||||
"channel_id": "channel_id",
|
||||
"last_update_at": 0,
|
||||
"last_viewed_at": 0,
|
||||
"mention_count": 0,
|
||||
"mention_count_root": 0,
|
||||
"msg_count": 0,
|
||||
"msg_count_root": 0,
|
||||
"notify_props": Object {
|
||||
"channel_auto_follow_threads": "off",
|
||||
"desktop": "default",
|
||||
"email": "default",
|
||||
"ignore_channel_mentions": "default",
|
||||
"mark_unread": "all",
|
||||
"push": "default",
|
||||
},
|
||||
"roles": "channel_user",
|
||||
"scheme_admin": false,
|
||||
"scheme_user": true,
|
||||
"urgent_mention_count": 0,
|
||||
"user_id": "user_id",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Connect(CallButton) />
|
||||
<ChannelInfoButton
|
||||
channel={
|
||||
Object {
|
||||
|
||||
@@ -49,6 +49,7 @@ describe('components/ChannelHeader', () => {
|
||||
'hour',
|
||||
],
|
||||
hideGuestTags: false,
|
||||
sharedChannelsPluginsEnabled: false,
|
||||
intl: {
|
||||
formatMessage: jest.fn(({id, defaultMessage}) => defaultMessage || id),
|
||||
} as MockIntl,
|
||||
|
||||
@@ -370,11 +370,15 @@ class ChannelHeader extends React.PureComponent<Props> {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChannelHeaderPlug
|
||||
channel={channel}
|
||||
channelMember={channelMember}
|
||||
/>
|
||||
<CallButton/>
|
||||
{(!channel.shared || this.props.sharedChannelsPluginsEnabled) && (
|
||||
<>
|
||||
<ChannelHeaderPlug
|
||||
channel={channel}
|
||||
channelMember={channelMember}
|
||||
/>
|
||||
<CallButton/>
|
||||
</>
|
||||
)}
|
||||
<ChannelInfoButton channel={channel}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
isCurrentChannelMuted,
|
||||
getCurrentChannelStats,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getConfig, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {
|
||||
displayLastActiveLabel,
|
||||
@@ -54,6 +54,7 @@ function makeMapStateToProps() {
|
||||
const channel = getCurrentChannel(state);
|
||||
const user = getCurrentUser(state);
|
||||
const config = getConfig(state);
|
||||
const sharedChannelsPluginsEnabled = getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true';
|
||||
|
||||
let dmUser;
|
||||
let gmMembers;
|
||||
@@ -96,6 +97,7 @@ function makeMapStateToProps() {
|
||||
isLastActiveEnabled,
|
||||
timestampUnits,
|
||||
hideGuestTags: config.HideGuestTags === 'true',
|
||||
sharedChannelsPluginsEnabled,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import ChannelPublicPrivateMenu from './channel_header_menu_items/channel_header
|
||||
|
||||
import ChannelHeaderTitleDirect from '../channel_header/channel_header_title_direct';
|
||||
import ChannelHeaderTitleGroup from '../channel_header/channel_header_title_group';
|
||||
import {usePluginVisibilityInSharedChannel} from '../common/hooks/usePluginVisibilityInSharedChannel';
|
||||
|
||||
type Props = {
|
||||
dmUser?: UserProfile;
|
||||
@@ -55,6 +56,7 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
|
||||
const isLicensedForLDAPGroups = useSelector(getLicense).LDAPGroups === 'true';
|
||||
const pluginMenuItems = useSelector(getChannelHeaderMenuPluginComponents);
|
||||
const isChannelBookmarksEnabled = useSelector(getIsChannelBookmarksEnabled);
|
||||
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channel?.id);
|
||||
|
||||
const isReadonly = false;
|
||||
|
||||
@@ -86,22 +88,26 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
|
||||
channelTitle = <ChannelHeaderTitleGroup gmMembers={gmMembers}/>;
|
||||
}
|
||||
|
||||
const pluginItems = pluginMenuItems.map((item) => {
|
||||
const handlePluginItemClick = () => {
|
||||
if (item.action) {
|
||||
item.action(channel.id);
|
||||
}
|
||||
};
|
||||
let pluginItems: JSX.Element[] = [];
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id={item.id + '_pluginmenuitem'}
|
||||
key={item.id + '_pluginmenuitem'}
|
||||
onClick={handlePluginItemClick}
|
||||
labels={<span>{item.text}</span>}
|
||||
/>
|
||||
);
|
||||
});
|
||||
if (pluginItemsVisible) {
|
||||
pluginItems = pluginMenuItems.map((item) => {
|
||||
const handlePluginItemClick = () => {
|
||||
if (item.action) {
|
||||
item.action(channel.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id={item.id + '_pluginmenuitem'}
|
||||
key={item.id + '_pluginmenuitem'}
|
||||
onClick={handlePluginItemClick}
|
||||
labels={<span>{item.text}</span>}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.Container
|
||||
@@ -183,4 +189,3 @@ export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archived
|
||||
</Menu.Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||
import CopyButton from 'components/copy_button';
|
||||
|
||||
import * as SyntaxHighlighting from 'utils/syntax_highlighting';
|
||||
@@ -15,9 +16,10 @@ type Props = {
|
||||
code: string;
|
||||
language: string;
|
||||
searchedContent?: string;
|
||||
channelId?: string;
|
||||
}
|
||||
|
||||
const CodeBlock: React.FC<Props> = ({code, language, searchedContent}: Props) => {
|
||||
const CodeBlock: React.FC<Props> = ({code, language, searchedContent, channelId}: Props) => {
|
||||
const getUsedLanguage = useCallback(() => {
|
||||
let usedLanguage = language || '';
|
||||
usedLanguage = usedLanguage.toLowerCase();
|
||||
@@ -82,7 +84,9 @@ const CodeBlock: React.FC<Props> = ({code, language, searchedContent}: Props) =>
|
||||
}
|
||||
|
||||
const codeBlockActions = useSelector((state: GlobalState) => state.plugins.components.CodeBlockAction);
|
||||
const pluginItems = codeBlockActions?.
|
||||
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channelId);
|
||||
|
||||
const pluginItems = pluginItemsVisible ? codeBlockActions?.
|
||||
map((item) => {
|
||||
if (!item.component) {
|
||||
return null;
|
||||
@@ -95,7 +99,7 @@ const CodeBlock: React.FC<Props> = ({code, language, searchedContent}: Props) =>
|
||||
code={code}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}) : [];
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {renderHook} from '@testing-library/react-hooks';
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {usePluginVisibilityInSharedChannel} from './usePluginVisibilityInSharedChannel';
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/channels', () => ({
|
||||
getChannel: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/general', () => ({
|
||||
getFeatureFlagValue: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('usePluginVisibilityInSharedChannel', () => {
|
||||
const mockStore = configureStore();
|
||||
const mockGetChannel = getChannel as jest.MockedFunction<typeof getChannel>;
|
||||
const mockGetFeatureFlagValue = getFeatureFlagValue as jest.MockedFunction<typeof getFeatureFlagValue>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetFeatureFlagValue.mockReturnValue('false');
|
||||
mockGetChannel.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderHookWithChannelId = (channelId: string | undefined) => {
|
||||
const store = mockStore({});
|
||||
const wrapper = ({children}: {children: React.ReactNode}) => (
|
||||
<Provider store={store}>{children}</Provider>
|
||||
);
|
||||
|
||||
return renderHook(() => usePluginVisibilityInSharedChannel(channelId), {wrapper});
|
||||
};
|
||||
|
||||
test('should return true when channelId is undefined', () => {
|
||||
const {result} = renderHookWithChannelId(undefined);
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true when channel is not found', () => {
|
||||
mockGetChannel.mockReturnValue(undefined);
|
||||
const {result} = renderHookWithChannelId('channel-id-1');
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for non-shared channels regardless of feature flag', () => {
|
||||
mockGetChannel.mockReturnValue({
|
||||
id: 'channel-id-1',
|
||||
shared: false,
|
||||
} as any);
|
||||
|
||||
const {result} = renderHookWithChannelId('channel-id-1');
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for shared channels when feature flag is disabled', () => {
|
||||
mockGetChannel.mockReturnValue({
|
||||
id: 'channel-id-1',
|
||||
shared: true,
|
||||
} as any);
|
||||
|
||||
const {result} = renderHookWithChannelId('channel-id-1');
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true for shared channels when feature flag is enabled', () => {
|
||||
mockGetChannel.mockReturnValue({
|
||||
id: 'channel-id-1',
|
||||
shared: true,
|
||||
} as any);
|
||||
mockGetFeatureFlagValue.mockReturnValue('true');
|
||||
|
||||
const {result} = renderHookWithChannelId('channel-id-1');
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for non-shared channels when feature flag is enabled', () => {
|
||||
mockGetChannel.mockReturnValue({
|
||||
id: 'channel-id-1',
|
||||
shared: false,
|
||||
} as any);
|
||||
mockGetFeatureFlagValue.mockReturnValue('true');
|
||||
|
||||
const {result} = renderHookWithChannelId('channel-id-1');
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle channels without shared property', () => {
|
||||
mockGetChannel.mockReturnValue({
|
||||
id: 'channel-id-1',
|
||||
|
||||
// no shared property - should default to false
|
||||
} as any);
|
||||
|
||||
const {result} = renderHookWithChannelId('channel-id-1');
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
/**
|
||||
* Custom hook to determine if plugin components should be visible in a channel.
|
||||
*
|
||||
* @param channelId - The ID of the channel to check (optional)
|
||||
* @returns true if plugins should be visible, false otherwise
|
||||
*
|
||||
* Plugins are visible when:
|
||||
* - The channel ID is undefined/null (defaults to visible), OR
|
||||
* - The channel is not shared, OR
|
||||
* - The channel is shared AND the EnableSharedChannelsPlugins feature flag is enabled
|
||||
*/
|
||||
export function usePluginVisibilityInSharedChannel(channelId: string | undefined): boolean {
|
||||
const channel = useSelector((state: GlobalState) =>
|
||||
(channelId ? getChannel(state, channelId) : undefined),
|
||||
);
|
||||
|
||||
const sharedChannelsPluginsEnabled = useSelector((state: GlobalState) =>
|
||||
getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true',
|
||||
);
|
||||
|
||||
// If no channel ID provided or channel not found, default to showing plugins
|
||||
if (!channelId || !channel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !channel.shared || sharedChannelsPluginsEnabled;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import type {GlobalState} from '@mattermost/types/store';
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import FileAttachment from './file_attachment';
|
||||
|
||||
@@ -58,6 +59,7 @@ describe('FileAttachment', () => {
|
||||
enableSVGs: false,
|
||||
enablePublicLink: false,
|
||||
pluginMenuItems: [],
|
||||
currentChannel: TestHelper.getChannelMock(),
|
||||
handleFileDropdownOpened: jest.fn(() => null),
|
||||
actions: {
|
||||
openModal: jest.fn(),
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {FileInfo} from '@mattermost/types/files';
|
||||
|
||||
import {getFileThumbnailUrl, getFileUrl} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||
import GetPublicModal from 'components/get_public_link_modal';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
@@ -69,6 +70,8 @@ export default function FileAttachment(props: Props) {
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const pluginItemsVisible = usePluginVisibilityInSharedChannel(props.currentChannel?.id);
|
||||
|
||||
const handleImageLoaded = () => {
|
||||
if (mounted.current) {
|
||||
setLoaded(true);
|
||||
@@ -195,16 +198,19 @@ export default function FileAttachment(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const pluginItems = pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => {
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
id={item.id + '_pluginmenuitem'}
|
||||
key={item.id + '_pluginmenuitem'}
|
||||
onClick={() => item?.action(fileInfo)}
|
||||
text={item.text}
|
||||
/>
|
||||
);
|
||||
});
|
||||
let pluginItems: JSX.Element[] = [];
|
||||
if (pluginItemsVisible) {
|
||||
pluginItems = pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => {
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
id={item.id + '_pluginmenuitem'}
|
||||
key={item.id + '_pluginmenuitem'}
|
||||
onClick={() => item?.action(fileInfo)}
|
||||
text={item.text}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const isMenuVisible = defaultItems?.length || pluginItems?.length;
|
||||
if (!isMenuVisible) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {ConnectedProps} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
@@ -29,6 +30,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
enableSVGs: config.EnableSVGs === 'true',
|
||||
enablePublicLink: config.EnablePublicLink === 'true',
|
||||
pluginMenuItems: getFilesDropdownPluginMenuItems(state),
|
||||
currentChannel: getCurrentChannel(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ describe('components/file_search_result/FileSearchResultItem', () => {
|
||||
channelDisplayName: '',
|
||||
channelType: Constants.OPEN_CHANNEL as ChannelType,
|
||||
teamName: 'test-team-name',
|
||||
channel: TestHelper.getChannelMock(),
|
||||
enableSharedChannelsPlugins: false,
|
||||
onClick: jest.fn(),
|
||||
actions: {
|
||||
openModal: jest.fn(),
|
||||
|
||||
@@ -58,7 +58,13 @@ export default class FileSearchResultItem extends React.PureComponent<Props, Sta
|
||||
};
|
||||
|
||||
private renderPluginItems = () => {
|
||||
const {fileInfo} = this.props;
|
||||
const {fileInfo, channel, enableSharedChannelsPlugins} = this.props;
|
||||
const isSharedChannel = channel?.shared || false;
|
||||
|
||||
if (isSharedChannel && !enableSharedChannelsPlugins) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pluginItems = this.props.pluginMenuItems?.filter((item) => item?.match(fileInfo)).map((item) => {
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {Dispatch} from 'redux';
|
||||
import type {FileInfo} from '@mattermost/types/files';
|
||||
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
@@ -26,10 +27,13 @@ export type OwnProps = {
|
||||
|
||||
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
const channel = getChannel(state, ownProps.channelId);
|
||||
const enableSharedChannelsPlugins = getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true';
|
||||
|
||||
return {
|
||||
channelDisplayName: '',
|
||||
channelType: channel?.type,
|
||||
channel,
|
||||
enableSharedChannelsPlugins,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@ function makeMapStateToProps() {
|
||||
canReply,
|
||||
pluginPostTypes: state.plugins.postTypes,
|
||||
channelIsArchived: isArchivedChannel(channel),
|
||||
channelIsShared: channel?.shared,
|
||||
isConsecutivePost: isConsecutivePost(state, ownProps),
|
||||
previousPostIsComment,
|
||||
isFlagged: isPostFlagged(state, post.id),
|
||||
|
||||
@@ -69,6 +69,7 @@ export type Props = {
|
||||
isReadOnly?: boolean;
|
||||
pluginPostTypes?: {[postType: string]: PostPluginComponent};
|
||||
channelIsArchived?: boolean;
|
||||
channelIsShared?: boolean;
|
||||
isConsecutivePost?: boolean;
|
||||
isLastPost?: boolean;
|
||||
recentEmojis: Emoji[];
|
||||
|
||||
@@ -14,6 +14,7 @@ import {isPostEphemeral} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import ActionsMenu from 'components/actions_menu';
|
||||
import CommentIcon from 'components/common/comment_icon';
|
||||
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||
import DotMenu from 'components/dot_menu';
|
||||
import PostFlagIcon from 'components/post_view/post_flag_icon';
|
||||
import PostReaction from 'components/post_view/post_reaction';
|
||||
@@ -32,6 +33,7 @@ type Props = {
|
||||
enableEmojiPicker?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
channelIsArchived?: boolean;
|
||||
channelIsShared?: boolean;
|
||||
handleCommentClick?: (e: React.MouseEvent) => void;
|
||||
handleJumpClick?: (e: React.MouseEvent) => void;
|
||||
handleDropdownOpened?: (e: boolean) => void;
|
||||
@@ -200,7 +202,9 @@ const PostOptions = (props: Props): JSX.Element => {
|
||||
);
|
||||
|
||||
let pluginItems: ReactNode = null;
|
||||
if ((!isEphemeral && !post.failed && !systemMessage) && hoverLocal) {
|
||||
const pluginItemsVisible = usePluginVisibilityInSharedChannel(post.channel_id);
|
||||
|
||||
if ((!isEphemeral && !post.failed && !systemMessage) && hoverLocal && pluginItemsVisible) {
|
||||
pluginItems = props.pluginActions?.
|
||||
map((item) => {
|
||||
if (item.component) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import React from 'react';
|
||||
|
||||
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||
|
||||
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||
|
||||
import type {ChannelIntroButtonAction} from 'types/store/plugins';
|
||||
|
||||
type Props = {
|
||||
@@ -19,6 +21,12 @@ const PluggableIntroButtons = React.memo(({
|
||||
channelMember,
|
||||
}: Props) => {
|
||||
const channelIsArchived = channel.delete_at !== 0;
|
||||
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channel.id);
|
||||
|
||||
if (!pluginItemsVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (channelIsArchived || pluginButtons.length === 0 || !channelMember) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getTheme, getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
@@ -21,6 +22,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
pluginPostTypes: state.plugins.postTypes,
|
||||
theme: getTheme(state),
|
||||
currentRelativeTeamUrl: getCurrentRelativeTeamUrl(state),
|
||||
sharedChannelsPluginsEnabled: getFeatureFlagValue(state, 'EnableSharedChannelsPlugins') === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,12 @@ import {FormattedMessage} from 'react-intl';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isPostEphemeral} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import store from 'stores/redux_store';
|
||||
|
||||
import PostMarkdown from 'components/post_markdown';
|
||||
import ShowMore from 'components/post_view/show_more';
|
||||
import type {AttachmentTextOverflowType} from 'components/post_view/show_more/show_more';
|
||||
@@ -36,6 +39,7 @@ type Props = {
|
||||
overflowType?: AttachmentTextOverflowType;
|
||||
maxHeight?: number; /* The max height used by the show more component */
|
||||
showPostEditedIndicator?: boolean; /* Whether or not to render the post edited indicator */
|
||||
sharedChannelsPluginsEnabled?: boolean;
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -142,6 +146,10 @@ export default class PostMessageView extends React.PureComponent<Props, State> {
|
||||
|
||||
const id = isRHS ? `rhsPostMessageText_${post.id}` : `postMessageText_${post.id}`;
|
||||
|
||||
// Check if channel is shared
|
||||
const channel = getChannel(store.getState(), post.channel_id);
|
||||
const isSharedChannel = channel?.shared || false;
|
||||
|
||||
return (
|
||||
<ShowMore
|
||||
checkOverflow={this.state.checkOverflow}
|
||||
@@ -164,11 +172,13 @@ export default class PostMessageView extends React.PureComponent<Props, State> {
|
||||
showPostEditedIndicator={this.props.showPostEditedIndicator}
|
||||
/>
|
||||
</div>
|
||||
<Pluggable
|
||||
pluggableName='PostMessageAttachment'
|
||||
postId={post.id}
|
||||
onHeightChange={this.handleHeightReceived}
|
||||
/>
|
||||
{(!isSharedChannel || this.props.sharedChannelsPluginsEnabled) && (
|
||||
<Pluggable
|
||||
pluggableName='PostMessageAttachment'
|
||||
postId={post.id}
|
||||
onHeightChange={this.handleHeightReceived}
|
||||
/>
|
||||
)}
|
||||
</ShowMore>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import {getMembershipForEntities} from 'actions/views/profile_popover';
|
||||
import {getSelectedPost} from 'selectors/rhs';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
import {usePluginVisibilityInSharedChannel} from 'components/common/hooks/usePluginVisibilityInSharedChannel';
|
||||
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import {A11yCustomEventTypes, UserStatuses} from 'utils/constants';
|
||||
@@ -73,6 +75,7 @@ const ProfilePopover = ({
|
||||
const user = useSelector((state: GlobalState) => getUser(state, userId));
|
||||
const currentTeamId = useSelector((state: GlobalState) => getCurrentTeamId(state));
|
||||
const channelId = useSelector((state: GlobalState) => (channelIdProp || getDefaultChannelId(state)));
|
||||
const pluginItemsVisible = usePluginVisibilityInSharedChannel(channelId);
|
||||
const isMobileView = useSelector(getIsMobileView);
|
||||
const teamUrl = useSelector(getCurrentRelativeTeamUrl);
|
||||
const modals = useSelector((state: GlobalState) => state.views.modals);
|
||||
@@ -182,15 +185,17 @@ const ProfilePopover = ({
|
||||
haveOverrideProp={haveOverrideProp}
|
||||
isBot={user.is_bot}
|
||||
/>
|
||||
<div className='user-profile-popover-pluggables'>
|
||||
<Pluggable
|
||||
pluggableName={PLUGGABLE_COMPONENT_NAME_PROFILE_POPOVER}
|
||||
user={user}
|
||||
hide={hide}
|
||||
status={hideStatus ? null : status}
|
||||
fromWebhook={fromWebhook}
|
||||
/>
|
||||
</div>
|
||||
{pluginItemsVisible && (
|
||||
<div className='user-profile-popover-pluggables'>
|
||||
<Pluggable
|
||||
pluggableName={PLUGGABLE_COMPONENT_NAME_PROFILE_POPOVER}
|
||||
user={user}
|
||||
hide={hide}
|
||||
status={hideStatus ? null : status}
|
||||
fromWebhook={fromWebhook}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{enableCustomProfileAttributes && !user.is_bot && (
|
||||
<ProfilePopoverCustomAttributes
|
||||
@@ -238,12 +243,14 @@ const ProfilePopover = ({
|
||||
user={user}
|
||||
hide={hide}
|
||||
/>
|
||||
<Pluggable
|
||||
pluggableName='PopoverUserActions'
|
||||
user={user}
|
||||
hide={hide}
|
||||
status={hideStatus ? null : status}
|
||||
/>
|
||||
{pluginItemsVisible && (
|
||||
<Pluggable
|
||||
pluggableName='PopoverUserActions'
|
||||
user={user}
|
||||
hide={hide}
|
||||
status={hideStatus ? null : status}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -36,6 +36,7 @@ export type Options = Partial<{
|
||||
images: boolean;
|
||||
atPlanMentions: boolean;
|
||||
channelId: string;
|
||||
channelIsShared: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the AtMention component should attempt to fetch at-mentioned users if none can be found for
|
||||
@@ -267,6 +268,7 @@ export default function messageHtmlToComponent(html: string, options: Options =
|
||||
code={node.attribs['data-codeblock-code']}
|
||||
language={node.attribs['data-codeblock-language']}
|
||||
searchedContent={node.attribs['data-codeblock-searchedcontent']}
|
||||
channelId={options.channelId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
Ссылка в новой задаче
Block a user