* Fix pluggables types

* Address feedback

* Fix lint
Этот коммит содержится в:
Daniel Espino García
2025-01-17 09:47:12 +01:00
коммит произвёл GitHub
родитель cb75a20c54
Коммит 4a59d4c08e
60 изменённых файлов: 996 добавлений и 1501 удалений

Просмотреть файл

@@ -39,8 +39,9 @@ exports[`components/actions_menu/ActionsMenu has actions - marketplace disabled
key="the_component_id_pluginmenuitem"
onClick={[Function]}
show={true}
text="Some text"
/>
<Connect(Pluggable)
<Pluggable
key="post_id_1pluggable"
pluggableName="PostDropdownMenuItem"
postId="post_id_1"
@@ -88,8 +89,9 @@ exports[`components/actions_menu/ActionsMenu has actions - marketplace enabled a
key="the_component_id_pluginmenuitem"
onClick={[Function]}
show={true}
text="Some text"
/>
<Connect(Pluggable)
<Pluggable
key="post_id_1pluggable"
pluggableName="PostDropdownMenuItem"
postId="post_id_1"

Просмотреть файл

@@ -8,9 +8,9 @@ import type {PostType} from '@mattermost/types/posts';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import {TestHelper} from 'utils/test_helper';
import type {PluginComponent} from 'types/store/plugins';
import type {PostDropdownMenuAction} from 'types/store/plugins';
import ActionsMenu, {PLUGGABLE_COMPONENT} from './actions_menu';
import ActionsMenu from './actions_menu';
import type {Props} from './actions_menu';
jest.mock('utils/utils', () => {
@@ -21,11 +21,13 @@ jest.mock('utils/utils', () => {
};
});
const dropdownComponents: PluginComponent[] = [
const dropdownComponents: PostDropdownMenuAction[] = [
{
id: 'the_component_id',
pluginId: 'playbooks',
text: 'Some text',
action: jest.fn(),
filter: () => true,
},
];
@@ -39,7 +41,7 @@ describe('components/actions_menu/ActionsMenu', () => {
isSysAdmin: true,
pluginMenuItems: [],
post: TestHelper.getPostMock({id: 'post_id_1', is_pinned: false, type: '' as PostType}),
components: {},
pluginMenuItemComponents: [],
location: 'center',
canOpenMarketplace: false,
actions: {
@@ -117,9 +119,7 @@ describe('components/actions_menu/ActionsMenu', () => {
expect(wrapper.find('#divider_post_post_id_1_marketplace').exists()).toBe(false);
wrapper.setProps({
components: {
[PLUGGABLE_COMPONENT]: dropdownComponents,
},
pluginMenuItemComponents: dropdownComponents,
canOpenMarketplace: true,
});
expect(wrapper.find('#divider_post_post_id_1_marketplace').exists()).toBe(true);
@@ -135,9 +135,7 @@ describe('components/actions_menu/ActionsMenu', () => {
expect(wrapper.find('#divider_post_post_id_1_marketplace').exists()).toBe(false);
wrapper.setProps({
components: {
[PLUGGABLE_COMPONENT]: dropdownComponents,
},
pluginMenuItemComponents: dropdownComponents,
});
expect(wrapper.find('#divider_post_post_id_1_marketplace').exists()).toBe(false);
});

Просмотреть файл

@@ -27,12 +27,12 @@ import * as PostUtils from 'utils/post_utils';
import type {ModalData} from 'types/actions';
import type {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForPost} from 'types/apps';
import type {PluginComponent} from 'types/store/plugins';
import './actions_menu.scss';
import type {PostDropdownMenuAction, PostDropdownMenuItemComponent} from 'types/store/plugins';
import {ActionsMenuIcon} from './actions_menu_icon';
import './actions_menu.scss';
const MENU_BOTTOM_MARGIN = 80;
export const PLUGGABLE_COMPONENT = 'PostDropdownMenuItem';
@@ -44,7 +44,7 @@ export type Props = {
isMenuOpen?: boolean;
isSysAdmin: boolean;
location?: 'CENTER' | 'RHS_ROOT' | 'RHS_COMMENT' | 'SEARCH' | string;
pluginMenuItems?: PluginComponent[];
pluginMenuItems?: PostDropdownMenuAction[];
post: Post;
teamId: string;
canOpenMarketplace: boolean;
@@ -52,9 +52,7 @@ export type Props = {
/**
* Components for overriding provided by plugins
*/
components: {
[componentName: string]: PluginComponent[];
};
pluginMenuItemComponents: PostDropdownMenuItemComponent[];
actions: {
@@ -346,7 +344,7 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
let menuItems;
const hasApps = Boolean(appBindings.length);
const hasPluggables = Boolean(this.props.components[PLUGGABLE_COMPONENT]?.length);
const hasPluggables = Boolean(this.props.pluginMenuItemComponents?.length);
const hasPluginItems = Boolean(pluginItems?.length);
const hasPluginMenuItems = hasPluginItems || hasApps || hasPluggables;

Просмотреть файл

@@ -28,7 +28,6 @@ describe('components/actions_menu/ActionsMenu returning empty ("")', () => {
test('should match snapshot, return empty ("") on Center', () => {
const baseProps: Omit<Props, 'intl'> = {
post: TestHelper.getPostMock({id: 'post_id_1'}),
components: {},
teamId: 'team_id_1',
actions: {
openModal: jest.fn(),
@@ -42,6 +41,7 @@ describe('components/actions_menu/ActionsMenu returning empty ("")', () => {
appsEnabled: false,
isSysAdmin: true,
canOpenMarketplace: false,
pluginMenuItemComponents: [],
};
const wrapper = shallow(

Просмотреть файл

@@ -28,7 +28,6 @@ describe('components/actions_menu/ActionsMenu on mobile view', () => {
test('should match snapshot', () => {
const baseProps: Omit<Props, 'intl'> = {
post: TestHelper.getPostMock({id: 'post_id_1'}),
components: {},
teamId: 'team_id_1',
actions: {
openModal: jest.fn(),
@@ -42,6 +41,7 @@ describe('components/actions_menu/ActionsMenu on mobile view', () => {
appsEnabled: false,
isSysAdmin: true,
canOpenMarketplace: false,
pluginMenuItemComponents: [],
};
const wrapper = shallow(

Просмотреть файл

@@ -60,7 +60,7 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
return {
appBindings,
appsEnabled: apps,
components: state.plugins.components,
pluginMenuItemComponents: state.plugins.components.PostDropdownMenuItem,
isSysAdmin,
pluginMenuItems: state.plugins.components.PostDropdownMenu,
teamId: getCurrentTeamId(state),

Просмотреть файл

@@ -12,27 +12,28 @@ import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {PluginComponent} from 'types/store/plugins';
import type {ChannelHeaderButtonAction, RightHandSidebarComponent} from 'types/store/plugins';
import AppBar from './app_bar';
describe('components/app_bar/app_bar', () => {
const channelHeaderComponents: PluginComponent[] = [
const channelHeaderComponents: ChannelHeaderButtonAction[] = [
{
id: 'the_component_id',
pluginId: 'playbooks',
icon: 'fallback_component' as any,
tooltipText: 'Playbooks Tooltip',
action: jest.fn(),
dropdownText: 'Playbooks dropdown',
},
];
const rhsComponents: PluginComponent[] = [
const rhsComponents: RightHandSidebarComponent[] = [
{
id: 'the_rhs_plugin_component_id',
pluginId: 'playbooks',
icon: <div/>,
action: jest.fn(),
component: () => null,
title: 'some title',
},
];
@@ -61,7 +62,7 @@ describe('components/app_bar/app_bar', () => {
AppBar: channelHeaderComponents,
RightHandSidebarComponent: rhsComponents,
Product: [],
} as {[componentName: string]: PluginComponent[]},
},
},
entities: {
apps: {

Просмотреть файл

@@ -3,7 +3,6 @@
import partition from 'lodash/partition';
import React from 'react';
import type {ReactNode} from 'react';
import {useSelector} from 'react-redux';
import type {GlobalState} from '@mattermost/types/store';
@@ -20,7 +19,7 @@ import {useCurrentProduct, useCurrentProductId, inScope} from 'utils/products';
import AppBarBinding, {isAppBinding} from './app_bar_binding';
import AppBarMarketplace from './app_bar_marketplace';
import AppBarPluginComponent, {isAppBarPluginComponent} from './app_bar_plugin_component';
import AppBarPluginComponent, {isAppBarComponent} from './app_bar_plugin_component';
import './app_bar.scss';
@@ -49,7 +48,7 @@ export default function AppBar() {
return coreProductsPluginIds.includes(pluginId);
});
const items: ReactNode[] = [
const items = [
...coreProductComponents,
getDivider(coreProductComponents.length, (pluginComponents.length + channelHeaderComponents.length + appBarBindings.length)),
...pluginComponents,
@@ -60,8 +59,9 @@ export default function AppBar() {
return x;
}
if (isAppBarPluginComponent(x)) {
if (!inScope(x.supportedProductIds ?? null, currentProductId, currentProduct?.pluginId)) {
if (isAppBarComponent(x)) {
const supportedProductIds = 'supportedProductIds' in x ? x.supportedProductIds : undefined;
if (!inScope(supportedProductIds ?? null, currentProductId, currentProduct?.pluginId)) {
return null;
}
return (

Просмотреть файл

@@ -14,12 +14,12 @@ import WithTooltip from 'components/with_tooltip';
import {suitePluginIds} from 'utils/constants';
import type {PluginComponent, AppBarComponent} from 'types/store/plugins';
import type {AppBarAction, ChannelHeaderButtonAction} from 'types/store/plugins';
import NewChannelWithBoardTourTip from './new_channel_with_board_tour_tip';
type PluginComponentProps = {
component: AppBarComponent;
type AppBarComponentProps = {
component: ChannelHeaderButtonAction | AppBarAction;
}
enum ImageLoadState {
@@ -28,22 +28,27 @@ enum ImageLoadState {
ERROR = 'error',
}
export const isAppBarPluginComponent = (x: Record<string, any> | undefined): x is PluginComponent => {
export const isAppBarComponent = (x: Record<string, any> | undefined): x is (ChannelHeaderButtonAction | AppBarAction) => {
return Boolean(x?.id && x?.pluginId);
};
const AppBarPluginComponent = (props: PluginComponentProps) => {
const {component} = props;
const AppBarPluginComponent = ({
component,
}: AppBarComponentProps) => {
const channel = useSelector(getCurrentChannel);
const channelMember = useSelector(getMyCurrentChannelMembership);
const activeRhsComponent = useSelector(getActiveRhsComponent);
const [imageLoadState, setImageLoadState] = useState<ImageLoadState>(ImageLoadState.LOADING);
const iconUrl = 'iconUrl' in component ? component.iconUrl : undefined;
const icon = 'icon' in component ? component.icon : undefined;
const dropdownText = 'dropdownText' in component ? component.dropdownText : undefined;
const rhsComponentId = 'rhsComponentId' in component ? component.rhsComponentId : undefined;
useEffect(() => {
setImageLoadState(ImageLoadState.LOADING);
}, [component.iconUrl]);
}, [iconUrl]);
const onImageLoadComplete = () => {
setImageLoadState(ImageLoadState.LOADED);
@@ -54,9 +59,8 @@ const AppBarPluginComponent = (props: PluginComponentProps) => {
};
const buttonId = `app-bar-icon-${component.pluginId}`;
const tooltipText = component.tooltipText || component.dropdownText || component.pluginId;
const tooltipText = component.tooltipText || dropdownText || component.pluginId;
const iconUrl = component.iconUrl;
let content: React.ReactNode = (
<div
role='button'
@@ -72,7 +76,7 @@ const AppBarPluginComponent = (props: PluginComponentProps) => {
</div>
);
const isButtonActive = component.rhsComponentId ? activeRhsComponent?.id === component.rhsComponentId : component.pluginId === activeRhsComponent?.pluginId;
const isButtonActive = rhsComponentId ? activeRhsComponent?.id === rhsComponentId : component.pluginId === activeRhsComponent?.pluginId;
if (!iconUrl) {
content = (
@@ -81,7 +85,7 @@ const AppBarPluginComponent = (props: PluginComponentProps) => {
tabIndex={0}
className={classNames('app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered', {'app-bar__old-icon--active': isButtonActive})}
>
{component.icon}
{icon}
</div>
);
}
@@ -103,7 +107,13 @@ const AppBarPluginComponent = (props: PluginComponentProps) => {
id={buttonId}
className={classNames('app-bar__icon', {'app-bar__icon--active': isButtonActive})}
onClick={() => {
component.action?.(channel, channelMember);
if (channel && channelMember) {
component.action?.(channel, channelMember);
return;
}
if ('rhsComponentId' in component) {
component.action();
}
}}
>
{content}

Просмотреть файл

@@ -1,7 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {type ComponentType} from 'react';
import React, {lazy, type ComponentType} from 'react';
import type {PluggableComponentType, PluggableProps} from 'plugins/pluggable/pluggable';
import type {PluginsState, ProductSubComponentNames} from 'types/store/plugins';
export function makeAsyncComponent<ComponentProps>(displayName: string, LazyComponent: React.ComponentType<ComponentProps>, fallback: React.ReactNode = null) {
const Component: ComponentType<ComponentProps> = (props) => (
@@ -12,3 +16,17 @@ export function makeAsyncComponent<ComponentProps>(displayName: string, LazyComp
Component.displayName = displayName;
return Component;
}
export function makeAsyncPluggableComponent() {
const LazyComponent = lazy(() => import('plugins/pluggable')) as PluggableComponentType;
const Component = <T extends keyof PluginsState['components'], U extends ProductSubComponentNames>(props: PluggableProps<T, U>) => (
<React.Suspense fallback={null}>
<LazyComponent<T, U> {...props}/>
</React.Suspense>
);
Component.displayName = 'Pluggable';
return Component;
}

Просмотреть файл

@@ -33,8 +33,8 @@ describe('components/ChannelHeaderDropdown', () => {
const props: Props = {
...defaultProps,
pluginMenuItems: [
{id: 'plugin-1', pluginId: 'playbooks', action: jest.fn(), text: 'plugin-1-text'},
{id: 'plugin-2', pluginId: 'playbooks', action: jest.fn(), text: 'plugin-2-text'},
{id: 'plugin-1', pluginId: 'playbooks', action: jest.fn(), text: 'plugin-1-text', shouldRender: () => true},
{id: 'plugin-2', pluginId: 'playbooks', action: jest.fn(), text: 'plugin-2-text', shouldRender: () => true},
],
};
const wrapper = shallow(<ChannelHeaderDropdown {...props}/>);

Просмотреть файл

@@ -30,7 +30,7 @@ import MobileChannelHeaderPlug from 'plugins/mobile_channel_header_plug';
import {Constants, ModalIdentifiers} from 'utils/constants';
import {localizeMessage} from 'utils/utils';
import type {PluginComponent} from 'types/store/plugins';
import type {ChannelHeaderAction} from 'types/store/plugins';
import MenuItemCloseChannel from './menu_items/close_channel';
import MenuItemCloseMessage from './menu_items/close_message';
@@ -51,7 +51,7 @@ export type Props = {
isArchived: boolean;
isMobile: boolean;
penultimateViewedChannelName: string;
pluginMenuItems: PluginComponent[];
pluginMenuItems: ChannelHeaderAction[];
isLicensedForLDAPGroups: boolean;
isChannelBookmarksEnabled: boolean;
}

Просмотреть файл

@@ -16,7 +16,6 @@ import {makeAsyncComponent} from 'components/async_load';
import {canDownloadFiles} from 'utils/file_utils';
import type {GlobalState} from 'types/store';
import type {FilePreviewComponent} from 'types/store/plugins';
import type {Props} from './file_preview_modal';
@@ -34,7 +33,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
canDownloadFiles: canDownloadFiles(config),
enablePublicLink: config.EnablePublicLink === 'true',
isMobileView: getIsMobileView(state),
pluginFilePreviewComponents: state.plugins.components.FilePreview as unknown as FilePreviewComponent[],
pluginFilePreviewComponents: state.plugins.components.FilePreview,
post: ownProps.post || getPost(state, ownProps.postId || ''),
};
}

Просмотреть файл

@@ -13,7 +13,7 @@ import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {openModal} from 'actions/views/modals';
import type {GlobalState} from 'types/store';
import type {FileDropdownPluginComponent} from 'types/store/plugins';
import type {FilesDropdownAction} from 'types/store/plugins';
import FileSearchResultItem from './file_search_result_item';
@@ -21,7 +21,7 @@ export type OwnProps = {
channelId: string;
fileInfo: FileInfo;
teamName: string;
pluginMenuItems?: FileDropdownPluginComponent[];
pluginMenuItems?: FilesDropdownAction[];
};
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {

Просмотреть файл

@@ -42,7 +42,7 @@ import {
isTextDroppableEvent,
} from 'utils/utils';
import type {FilesWillUploadHook, PluginComponent} from 'types/store/plugins';
import type {FilesWillUploadHook, FileUploadMethodAction} from 'types/store/plugins';
const holders = defineMessages({
limited: {
@@ -147,7 +147,7 @@ export type Props = {
/**
* Plugin file upload methods to be added
*/
pluginFileUploadMethods: PluginComponent[];
pluginFileUploadMethods: FileUploadMethodAction[];
pluginFilesWillUploadHooks: FilesWillUploadHook[];
/**

Просмотреть файл

@@ -14,7 +14,6 @@ import {getEditingPostDetailsAndPost} from 'selectors/posts';
import {canUploadFiles} from 'utils/file_utils';
import type {GlobalState} from 'types/store';
import type {FilesWillUploadHook} from 'types/store/plugins';
import FileUpload from './file_upload';
@@ -31,7 +30,7 @@ function mapStateToProps(state: GlobalState) {
canUploadFiles: canUploadFiles(config),
locale: getCurrentLocale(state),
pluginFileUploadMethods: state.plugins.components.FileUploadMethod,
pluginFilesWillUploadHooks: state.plugins.components.FilesWillUploadHook as unknown as FilesWillUploadHook[],
pluginFilesWillUploadHooks: state.plugins.components.FilesWillUploadHook,
centerChannelPostBeingEdited,
rhsPostBeingEdited,
};

Просмотреть файл

@@ -1899,6 +1899,7 @@ exports[`components/Menu should match snapshot with plugins 1`] = `
key="plugin-id-1_pluginmenuitem"
onClick={[Function]}
show={true}
text="some text"
/>
<MenuItemAction
icon={false}
@@ -1906,6 +1907,7 @@ exports[`components/Menu should match snapshot with plugins 1`] = `
key="plugind-id-2_pluginmenuitem"
onClick={[Function]}
show={true}
text="some text"
/>
</Memo(MenuGroup)>
</Menu>
@@ -2249,16 +2251,28 @@ exports[`components/Menu should match snapshot with plugins in mobile 1`] = `
</Memo(MenuGroup)>
<Memo(MenuGroup)>
<MenuItemAction
icon={
<i
className="fa fa-anchor"
/>
}
id="plugin-id-1_pluginmenuitem"
key="plugin-id-1_pluginmenuitem"
onClick={[Function]}
show={true}
text="some text"
/>
<MenuItemAction
icon={
<i
className="fa fa-anchor"
/>
}
id="plugind-id-2_pluginmenuitem"
key="plugind-id-2_pluginmenuitem"
onClick={[Function]}
show={true}
text="some text"
/>
</Memo(MenuGroup)>
<Memo(MenuGroup)>

Просмотреть файл

@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import type {ComponentProps} from 'react';
import React from 'react';
import {createIntl} from 'react-intl';
import {Provider} from 'react-redux';
@@ -12,7 +13,6 @@ import Menu from 'components/widgets/menu/menu';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {Constants} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import {MainMenu} from './main_menu';
@@ -28,24 +28,18 @@ describe('components/Menu', () => {
// return wrapper.find('MainMenu').shallow();
};
const defaultProps = {
const defaultProps: ComponentProps<typeof MainMenu> = {
mobile: false,
teamId: 'team-id',
teamType: Constants.OPEN_TEAM,
teamName: 'team_name',
currentUser: TestHelper.getUserMock(),
appDownloadLink: undefined,
enableCommands: false,
enableCustomEmoji: false,
enableIncomingWebhooks: false,
enableOAuthServiceProvider: false,
enableOutgoingWebhooks: false,
canManageSystemBots: false,
canCreateOrDeleteCustomEmoji: false,
canManageIntegrations: true,
enableUserCreation: false,
enableEmailInvitations: false,
enablePluginMarketplace: false,
experimentalPrimaryTeam: undefined,
helpLink: undefined,
reportAProblemLink: undefined,
@@ -61,13 +55,10 @@ describe('components/Menu', () => {
showFlaggedPosts: jest.fn(),
closeRightHandSide: jest.fn(),
closeRhsMenu: jest.fn(),
getCloudLimits: jest.fn(),
},
teamIsGroupConstrained: false,
isCloud: false,
isStarterFree: false,
subscription: {},
userIsAdmin: true,
isFreeTrial: false,
usageDeltaTeams: 1,
};
@@ -178,23 +169,21 @@ describe('components/Menu', () => {
});
test('should match snapshot with plugins', () => {
const props = {
const props: ComponentProps<typeof MainMenu> = {
...defaultProps,
pluginMenuItems: [{
id: 'plugin-id-1',
pluginId: 'plugin-1',
mobileIcon: <i className='fa fa-anchor'/>,
action: jest.fn,
dropdownText: 'some dropdown text',
tooltipText: 'some tooltip text',
text: 'some text',
},
{
id: 'plugind-id-2',
pluginId: 'plugin-2',
mobileIcon: <i className='fa fa-anchor'/>,
action: jest.fn,
dropdownText: 'some dropdown text',
tooltipText: 'some tooltip text',
text: 'some text',
},
],
};
@@ -203,24 +192,22 @@ describe('components/Menu', () => {
});
test('should match snapshot with plugins in mobile', () => {
const props = {
const props: ComponentProps<typeof MainMenu> = {
...defaultProps,
mobile: true,
pluginMenuItems: [{
id: 'plugin-id-1',
pluginId: 'plugin-1',
icon: <i className='fa fa-anchor'/>,
mobileIcon: <i className='fa fa-anchor'/>,
action: jest.fn,
dropdownText: 'some dropdown text',
tooltipText: 'some tooltip text',
text: 'some text',
},
{
id: 'plugind-id-2',
pluginId: 'plugin-2',
icon: <i className='fa fa-anchor'/>,
mobileIcon: <i className='fa fa-anchor'/>,
action: jest.fn,
dropdownText: 'some dropdown text',
tooltipText: 'some tooltip text',
text: 'some text',
},
],
};

Просмотреть файл

@@ -33,7 +33,7 @@ import {makeUrlSafe} from 'utils/url';
import * as UserAgent from 'utils/user_agent';
import type {ModalData} from 'types/actions';
import type {PluginComponent} from 'types/store/plugins';
import type {MainMenuAction} from 'types/store/plugins';
import LearnAboutTeamsLink from './learn_about_teams_link';
import './main_menu.scss';
@@ -56,7 +56,7 @@ export type Props = {
helpLink?: string;
reportAProblemLink?: string;
moreTeamsToJoin: boolean;
pluginMenuItems?: PluginComponent[];
pluginMenuItems?: MainMenuAction[];
isMentionSearch?: boolean;
teamIsGroupConstrained: boolean;
isLicensedForLDAPGroups?: boolean;

Просмотреть файл

@@ -74,8 +74,7 @@ const NewChannelModal = () => {
const [channelInputError, setChannelInputError] = useState(false);
// create a board along with the channel
const pluginsComponentsList = useSelector((state: GlobalState) => state.plugins.components);
const createBoardFromChannelPlugin = pluginsComponentsList?.CreateBoardFromTemplate;
const createBoardFromChannelPlugin = useSelector((state: GlobalState) => state.plugins.components.CreateBoardFromTemplate);
const newChannelWithBoardPulsatingDotState = useSelector((state: GlobalState) => getPreference(state, Preferences.APP_BAR, Preferences.NEW_CHANNEL_WITH_BOARD_TOUR_SHOWED, ''));
const [canCreateFromPluggable, setCanCreateFromPluggable] = useState(true);

Просмотреть файл

@@ -64,7 +64,7 @@ const SearchBoxHints = ({searchTerms, setSearchTerms, searchType, providerResult
return null;
}
const Component: any = pluginComponentInfo.component;
const Component = pluginComponentInfo.component;
return (
<ErrorBoundary>

Просмотреть файл

@@ -113,7 +113,7 @@ const SearchSuggestions = ({searchType, searchTerms, suggestionsHeader, provider
);
}
const pluginComponentInfo = searchPluginSuggestions.find(({pluginId}: any) => {
const pluginComponentInfo = searchPluginSuggestions.find(({pluginId}) => {
if (searchType === pluginId) {
return true;
}
@@ -124,7 +124,7 @@ const SearchSuggestions = ({searchType, searchTerms, suggestionsHeader, provider
return null;
}
const Component: any = pluginComponentInfo.component;
const Component = pluginComponentInfo.component;
return (
<ErrorBoundary>

Просмотреть файл

@@ -81,7 +81,7 @@ export default function PluginLinkTooltip(props: Props) {
{...getFloatingProps()}
>
<Pluggable
href={props.nodeAttributes.href}
href={props.nodeAttributes.href || ''}
show={true}
pluggableName='LinkTooltip'
/>

Просмотреть файл

@@ -47,7 +47,7 @@ import {isKeyPressed} from 'utils/keyboard';
import * as PostUtils from 'utils/post_utils';
import {getDateForUnixTicks, makeIsEligibleForClick} from 'utils/utils';
import type {PostPluginComponent, PluginComponent} from 'types/store/plugins';
import type {PostActionComponent, PostPluginComponent} from 'types/store/plugins';
import PostOptions from './post_options';
import PostUserProfile from './user_profile';
@@ -116,7 +116,7 @@ export type Props = {
isPostPriorityEnabled: boolean;
isCardOpen?: boolean;
canDelete?: boolean;
pluginActions: PluginComponent[];
pluginActions: PostActionComponent[];
};
const PostComponent = (props: Props): JSX.Element => {

Просмотреть файл

@@ -22,7 +22,7 @@ import PostRecentReactions from 'components/post_view/post_recent_reactions';
import {Locations, Constants} from 'utils/constants';
import {isSystemMessage, fromAutoResponder} from 'utils/post_utils';
import type {PluginComponent} from 'types/store/plugins';
import type {PostActionComponent} from 'types/store/plugins';
type Props = {
post: Post;
@@ -52,7 +52,7 @@ type Props = {
isPostHeaderVisible?: boolean | null;
isPostBeingEdited?: boolean;
canDelete?: boolean;
pluginActions: PluginComponent[];
pluginActions: PostActionComponent[];
actions: {
emitShortcutReactToLastPostFrom: (emittedFrom: 'CENTER' | 'RHS_ROOT' | 'NO_WHERE') => void;
};
@@ -191,7 +191,7 @@ const PostOptions = (props: Props): JSX.Element => {
pluginItems = props.pluginActions?.
map((item) => {
if (item.component) {
const Component = item.component as any;
const Component = item.component;
return (
<Component
post={props.post}

Просмотреть файл

@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ComponentProps} from 'react';
import React from 'react';
import type {Post, PostType} from '@mattermost/types/posts';
@@ -10,12 +11,10 @@ import {Posts} from 'mattermost-redux/constants';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {MessageWillFormatHook} from 'types/store/plugins';
import PostMarkdown from './post_markdown';
describe('components/PostMarkdown', () => {
const baseProps = {
const baseProps: ComponentProps<typeof PostMarkdown> = {
imageProps: {} as Record<string, unknown>,
pluginHooks: [],
message: 'message',
@@ -211,7 +210,7 @@ describe('components/PostMarkdown', () => {
});
test('plugin hooks can build upon other hook message updates', () => {
const props = {
const props: ComponentProps<typeof PostMarkdown> = {
...baseProps,
message: 'world',
post: TestHelper.getPostMock({
@@ -226,16 +225,20 @@ describe('components/PostMarkdown', () => {
}),
pluginHooks: [
{
id: 'some id',
pluginId: 'some plugin',
hook: (post: Post, updatedMessage: string) => {
return 'hello ' + updatedMessage;
},
},
{
id: 'different id',
pluginId: 'different plugin',
hook: (post: Post, updatedMessage: string) => {
return updatedMessage + '!';
},
},
] as MessageWillFormatHook[],
],
};
renderWithContext(<PostMarkdown {...props}/>, state);
expect(screen.queryByText('world', {exact: true})).not.toBeInTheDocument();
@@ -245,7 +248,7 @@ describe('components/PostMarkdown', () => {
});
test('plugin hooks can overwrite other hooks messages', () => {
const props = {
const props: ComponentProps<typeof PostMarkdown> = {
...baseProps,
message: 'world',
post: TestHelper.getPostMock({
@@ -260,16 +263,20 @@ describe('components/PostMarkdown', () => {
}),
pluginHooks: [
{
id: 'some id',
pluginId: 'some plugin',
hook: (post: Post) => {
return 'hello ' + post.message;
},
},
{
id: 'different id',
pluginId: 'different plugin',
hook: (post: Post) => {
return post.message + '!';
},
},
] as unknown as MessageWillFormatHook[],
],
};
renderWithContext(<PostMarkdown {...props}/>, state);
expect(screen.queryByText('world', {exact: true})).not.toBeInTheDocument();

Просмотреть файл

@@ -5,26 +5,30 @@ import React from 'react';
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
import type {PluginComponent} from 'types/store/plugins';
import type {ChannelIntroButtonAction} from 'types/store/plugins';
type Props = {
channel: Channel;
channelMember?: ChannelMembership;
pluginButtons: PluginComponent[];
pluginButtons: ChannelIntroButtonAction[];
}
const PluggableIntroButtons = React.memo((props: Props) => {
const channelIsArchived = props.channel.delete_at !== 0;
if (channelIsArchived || props.pluginButtons.length === 0) {
const PluggableIntroButtons = React.memo(({
channel,
pluginButtons,
channelMember,
}: Props) => {
const channelIsArchived = channel.delete_at !== 0;
if (channelIsArchived || pluginButtons.length === 0 || !channelMember) {
return null;
}
const buttons = props.pluginButtons.map((buttonProps) => {
const buttons = pluginButtons.map((buttonProps) => {
return (
<button
key={buttonProps.id}
className={'action-button'}
onClick={() => buttonProps.action?.(props.channel, props.channelMember)}
onClick={() => buttonProps.action?.(channel, channelMember)}
>
{buttonProps.icon}
{buttonProps.text}

Просмотреть файл

@@ -8,12 +8,12 @@ import * as PostList from 'mattermost-redux/utils/post_list';
import NotificationSeparator from 'components/widgets/separator/notification-separator';
import type {PluginComponent} from 'types/store/plugins';
import type {NewMessagesSeparatorActionComponent} from 'types/store/plugins';
type Props = {
separatorId: string;
wrapperRef?: React.RefObject<HTMLDivElement>;
newMessagesSeparatorActions: PluginComponent[];
newMessagesSeparatorActions: NewMessagesSeparatorActionComponent[];
channelId?: string;
threadId?: string;
}
@@ -33,7 +33,7 @@ const NewMessageSeparator = ({
return null;
}
const Component = item.component as any;
const Component = item.component;
return (
<Component
key={item.id}

Просмотреть файл

@@ -11,7 +11,6 @@ import {toggleEmbedVisibility} from 'actions/post_actions';
import {isEmbedVisible} from 'selectors/posts';
import type {GlobalState} from 'types/store';
import type {PostWillRenderEmbedPluginComponent} from 'types/store/plugins';
import PostBodyAdditionalContent from './post_body_additional_content';
import type {
@@ -21,7 +20,7 @@ import type {
function mapStateToProps(state: GlobalState, ownProps: Omit<Props, 'appsEnabled' | 'actions'>) {
return {
isEmbedVisible: isEmbedVisible(state, ownProps.post.id),
pluginPostWillRenderEmbedComponents: state.plugins.components.PostWillRenderEmbedComponent as unknown as PostWillRenderEmbedPluginComponent[],
pluginPostWillRenderEmbedComponents: state.plugins.components.PostWillRenderEmbedComponent,
appsEnabled: appsEnabled(state),
};
}

Просмотреть файл

@@ -20,13 +20,13 @@ import YoutubeVideo from 'components/youtube_video';
import webSocketClient from 'client/web_websocket_client';
import type {TextFormattingOptions} from 'utils/text_formatting';
import type {PostWillRenderEmbedPluginComponent} from 'types/store/plugins';
import type {PostWillRenderEmbedComponent} from 'types/store/plugins';
import EmbeddedBindings from '../embedded_bindings/embedded_bindings';
export type Props = {
post: Post;
pluginPostWillRenderEmbedComponents?: PostWillRenderEmbedPluginComponent[];
pluginPostWillRenderEmbedComponents?: PostWillRenderEmbedComponent[];
children?: JSX.Element;
isEmbedVisible?: boolean;
options?: Partial<TextFormattingOptions>;

Просмотреть файл

@@ -23,7 +23,7 @@ import NewMessageSeparator from 'components/post_view/new_message_separator/new_
import {PostListRowListIds, Locations} from 'utils/constants';
import {isIdNotPost} from 'utils/post_utils';
import type {PluginComponent} from 'types/store/plugins';
import type {NewMessagesSeparatorActionComponent} from 'types/store/plugins';
export type PostListRowProps = {
listId: string;
@@ -59,7 +59,7 @@ export type PostListRowProps = {
firstInaccessiblePostTime?: number;
channelId: string;
newMessagesSeparatorActions: PluginComponent[];
newMessagesSeparatorActions: NewMessagesSeparatorActionComponent[];
actions: {

Просмотреть файл

@@ -711,9 +711,7 @@ export default class PostList extends React.PureComponent<Props, State> {
{({height, width}) => (
<>
<div>
<Pluggable
pluggableName='ChannelToast'
/>
<Pluggable pluggableName='ChannelToast'/>
{this.renderToasts(width)}
</div>

Просмотреть файл

@@ -29,7 +29,7 @@ exports[`components/post_view/PostAttachment should match snapshot 1`] = `
}
/>
</div>
<Connect(Pluggable)
<Pluggable
onHeightChange={[Function]}
pluggableName="PostMessageAttachment"
postId="post_id"
@@ -66,7 +66,7 @@ exports[`components/post_view/PostAttachment should match snapshot, on Show Less
}
/>
</div>
<Connect(Pluggable)
<Pluggable
onHeightChange={[Function]}
pluggableName="PostMessageAttachment"
postId="post_id"
@@ -103,7 +103,7 @@ exports[`components/post_view/PostAttachment should match snapshot, on Show More
}
/>
</div>
<Connect(Pluggable)
<Pluggable
onHeightChange={[Function]}
pluggableName="PostMessageAttachment"
postId="post_id"
@@ -159,7 +159,7 @@ exports[`components/post_view/PostAttachment should match snapshot, on edited po
}
/>
</div>
<Connect(Pluggable)
<Pluggable
onHeightChange={[Function]}
pluggableName="PostMessageAttachment"
postId="post_id"
@@ -197,7 +197,7 @@ exports[`components/post_view/PostAttachment should match snapshot, on ephemeral
}
/>
</div>
<Connect(Pluggable)
<Pluggable
onHeightChange={[Function]}
pluggableName="PostMessageAttachment"
postId="post_id"

Просмотреть файл

@@ -18,6 +18,8 @@ import Pluggable from 'plugins/pluggable';
import type {TextFormattingOptions} from 'utils/text_formatting';
import * as Utils from 'utils/utils';
import type {PostPluginComponent} from 'types/store/plugins';
type Props = {
post: Post; /* The post to render the message for */
enableFormatting?: boolean; /* Set to enable Markdown formatting */
@@ -27,7 +29,9 @@ type Props = {
isRHSOpen?: boolean; /* Whether or not the RHS is visible */
isRHSExpanded?: boolean; /* Whether or not the RHS is expanded */
theme: Theme; /* Logged in user's theme */
pluginPostTypes?: any; /* Post type components from plugins */
pluginPostTypes?: {
[postType: string]: PostPluginComponent;
}; /* Post type components from plugins */
currentRelativeTeamUrl: string;
overflowType?: AttachmentTextOverflowType;
maxHeight?: number; /* The max height used by the show more component */

Просмотреть файл

@@ -10,11 +10,11 @@ import {createDirectChannel} from 'mattermost-redux/actions/channels';
import {Constants} from 'utils/constants';
import type {PluginComponent} from 'types/store/plugins';
import type {CallButtonAction} from 'types/store/plugins';
type Props = {
channelMember?: ChannelMembership;
pluginCallComponents: PluginComponent[];
pluginCallComponents: CallButtonAction[];
sidebarOpen: boolean;
currentUserId: string;
userId: string;

Просмотреть файл

@@ -18,7 +18,7 @@ import {Preferences} from 'mattermost-redux/constants';
import {measurePageLoadTelemetry, temporarilySetPageLoadContext, trackEvent, trackSelectorMetrics} from 'actions/telemetry_actions.jsx';
import BrowserStore from 'stores/browser_store';
import {makeAsyncComponent} from 'components/async_load';
import {makeAsyncComponent, makeAsyncPluggableComponent} from 'components/async_load';
import GlobalHeader from 'components/global_header/global_header';
import {HFRoute} from 'components/header_footer_route/header_footer_route';
import {HFTRoute, LoggedInHFTRoute} from 'components/header_footer_template_route';
@@ -68,7 +68,6 @@ const Authorize = makeAsyncComponent('Authorize', lazy(() => import('components/
const CreateTeam = makeAsyncComponent('CreateTeam', lazy(() => import('components/create_team')));
const Mfa = makeAsyncComponent('Mfa', lazy(() => import('components/mfa/mfa_controller')));
const PreparingWorkspace = makeAsyncComponent('PreparingWorkspace', lazy(() => import('components/preparing_workspace')));
const Pluggable = makeAsyncComponent('Pluggable', lazy(() => import('plugins/pluggable')));
const LaunchingWorkspace = makeAsyncComponent('LaunchingWorkspace', lazy(() => import('components/preparing_workspace/launching_workspace')));
const CompassThemeProvider = makeAsyncComponent('CompassThemeProvider', lazy(() => import('components/compass_theme_provider/compass_theme_provider')));
const TeamController = makeAsyncComponent('TeamController', lazy(() => import('components/team_controller')));
@@ -81,6 +80,8 @@ const ModalController = makeAsyncComponent('ModalController', lazy(() => import(
const AppBar = makeAsyncComponent('AppBar', lazy(() => import('components/app_bar/app_bar')));
const ComponentLibrary = makeAsyncComponent('ComponentLibrary', lazy(() => import('components/component_library')));
const Pluggable = makeAsyncPluggableComponent();
const noop = () => {};
export type Props = PropsFromRedux & RouteComponentProps;
@@ -558,7 +559,7 @@ export default class Root extends React.PureComponent<Props, State> {
{this.props.plugins?.map((plugin) => (
<Route
key={plugin.id}
path={'/plug/' + (plugin as any).route}
path={'/plug/' + plugin.route}
render={() => (
<Pluggable
pluggableName={'CustomRouteComponent'}

Просмотреть файл

@@ -32,7 +32,7 @@ exports[`components/sidebar should match snapshot 1`] = `
<div
className="sidebar--left__icons"
>
<Connect(Pluggable)
<Pluggable
pluggableName="LeftSidebarHeader"
/>
</div>
@@ -75,7 +75,7 @@ exports[`components/sidebar should match snapshot when direct channels modal is
<div
className="sidebar--left__icons"
>
<Connect(Pluggable)
<Pluggable
pluggableName="LeftSidebarHeader"
/>
</div>
@@ -122,7 +122,7 @@ exports[`components/sidebar should match snapshot when more channels modal is op
<div
className="sidebar--left__icons"
>
<Connect(Pluggable)
<Pluggable
pluggableName="LeftSidebarHeader"
/>
</div>

Просмотреть файл

@@ -21,7 +21,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn
>
channel_label
</span>
<Connect(Pluggable)
<Pluggable
channel={
Object {
"create_at": 0,
@@ -103,7 +103,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn
>
channel_label
</span>
<Connect(Pluggable)
<Pluggable
channel={
Object {
"create_at": 0,
@@ -189,7 +189,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn
channel_label
</span>
</WithTooltip>
<Connect(Pluggable)
<Pluggable
channel={
Object {
"create_at": 0,
@@ -271,7 +271,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn
>
channel_label
</span>
<Connect(Pluggable)
<Pluggable
channel={
Object {
"create_at": 0,

Просмотреть файл

@@ -13,7 +13,7 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
import {reconnect} from 'actions/websocket_actions.jsx';
import LocalStorageStore from 'stores/local_storage_store';
import {makeAsyncComponent} from 'components/async_load';
import {makeAsyncComponent, makeAsyncPluggableComponent} from 'components/async_load';
import ChannelController from 'components/channel_layout/channel_controller';
import useTelemetryIdentitySync from 'components/common/hooks/useTelemetryIdentifySync';
import InitialLoadingScreen from 'components/initial_loading_screen';
@@ -27,7 +27,7 @@ import {isIosSafari} from 'utils/user_agent';
import type {OwnProps, PropsFromRedux} from './index';
const BackstageController = makeAsyncComponent('BackstageController', lazy(() => import('components/backstage')));
const Pluggable = makeAsyncComponent('Pluggable', lazy(() => import('plugins/pluggable')));
const Pluggable = makeAsyncPluggableComponent();
const WAKEUP_CHECK_INTERVAL = 30000; // 30 seconds
const WAKEUP_THRESHOLD = 60000; // 60 seconds

Просмотреть файл

@@ -16,7 +16,7 @@ import type {Props as TimestampProps} from 'components/timestamp/timestamp';
import {Locations} from 'utils/constants';
import type {PluginComponent} from 'types/store/plugins';
import type {NewMessagesSeparatorActionComponent} from 'types/store/plugins';
import Reply from './reply';
@@ -30,7 +30,7 @@ type Props = {
previousPostId: string;
timestampProps?: Partial<TimestampProps>;
threadId: string;
newMessagesSeparatorActions: PluginComponent[];
newMessagesSeparatorActions: NewMessagesSeparatorActionComponent[];
};
function noop() {}

Просмотреть файл

@@ -21,11 +21,12 @@ import DelayedAction from 'utils/delayed_action';
import {getPreviousPostId, getLatestPostId} from 'utils/post_utils';
import * as Utils from 'utils/utils';
import type {PluginComponent} from 'types/store/plugins';
import type {NewMessagesSeparatorActionComponent} from 'types/store/plugins';
import type {FakePost} from 'types/store/rhs';
import CreateComment from './create_comment';
import Row from './thread_viewer_row';
import './virtualized_thread_viewer.scss';
type Props = {
@@ -40,7 +41,7 @@ type Props = {
useRelativeTimestamp: boolean;
isMobileView: boolean;
isThreadView: boolean;
newMessagesSeparatorActions: PluginComponent[];
newMessagesSeparatorActions: NewMessagesSeparatorActionComponent[];
inputPlaceholder?: string;
measureRhsOpened: () => void;
}

Просмотреть файл

@@ -48,8 +48,8 @@ export type Props = WrappedComponentProps & {
subMenu?: Menu[];
subMenuClass?: string;
icon?: React.ReactNode;
action?: (id?: string) => void;
filter?: (id?: string) => boolean;
action?: (id: string) => void;
filter?: (id: string) => boolean;
ariaLabel?: string;
root?: boolean;
show?: boolean;
@@ -111,14 +111,14 @@ export class SubMenuItem extends React.PureComponent<Props, State> {
}
showMobileSubMenuModal(subMenu);
} else if (action) { // leaf node in the tree handles action only
action(postId);
action(postId || '');
}
} else {
const shouldCallAction =
(event.type === 'keydown' && event.currentTarget.id === id) ||
event.target.parentElement.id === id;
if (shouldCallAction && action) {
action(postId);
action(postId || '');
}
}
};
@@ -153,7 +153,7 @@ export class SubMenuItem extends React.PureComponent<Props, State> {
const {id, postId, text, selectedValueText, subMenu, icon, filter, ariaLabel, direction, styleSelectableItem, extraText, renderSelected, rightDecorator, tabIndex, intl} = this.props;
const isMobile = isMobileViewHack();
if (filter && !filter(id)) {
if (filter && !filter(id || '')) {
return ('');
}

Просмотреть файл

@@ -15,14 +15,14 @@ import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import {Constants} from 'utils/constants';
import type {PluginComponent} from 'types/store/plugins';
import type {CallButtonAction} from 'types/store/plugins';
import './call_button.scss';
type Props = {
currentChannel?: Channel;
channelMember?: ChannelMembership;
pluginCallComponents: PluginComponent[];
pluginCallComponents: CallButtonAction[];
sidebarOpen: boolean;
}

Просмотреть файл

@@ -6,8 +6,6 @@ import React from 'react';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {PluginComponent} from 'types/store/plugins';
import ChannelHeaderPlug, {maxComponentsBeforeDropdown} from './channel_header_plug';
describe('plugins/ChannelHeaderPlug', () => {
@@ -26,7 +24,7 @@ describe('plugins/ChannelHeaderPlug', () => {
shouldShowAppBar: false,
};
function makeTestPlug(n = 1): PluginComponent {
function makeTestPlug(n = 1) {
return {
id: 'someid' + n,
pluginId: 'pluginid' + n,

Просмотреть файл

@@ -22,7 +22,7 @@ import {createCallContext} from 'utils/apps';
import {Constants} from 'utils/constants';
import type {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForChannel} from 'types/apps';
import type {PluginComponent} from 'types/store/plugins';
import type {ChannelHeaderButtonAction, PluggableText} from 'types/store/plugins';
type CustomMenuProps = {
open?: boolean;
@@ -100,7 +100,7 @@ class CustomToggle extends React.PureComponent<CustomToggleProps> {
type ChannelHeaderPlugProps = {
intl: IntlShape;
components: PluginComponent[];
components: ChannelHeaderButtonAction[];
appBindings?: AppBinding[];
appsEnabled: boolean;
channel: Channel;
@@ -164,16 +164,20 @@ class ChannelHeaderPlug extends React.PureComponent<ChannelHeaderPlugProps, Chan
this.onClose();
};
createComponentButton = (plug: PluginComponent) => {
createComponentButton = (plug: ChannelHeaderButtonAction) => {
// These values are supposed to be strings based on PluginComponent, but some plugins pass non-strings,
// so do some hacky stuff to try to convert it back to a string. DO NOT USE THIS ELSEWHERE!
function tooltipToAriaLabelHack(intl: IntlShape, stringOrElement: string | React.ReactElement) {
function tooltipToAriaLabelHack(intl: IntlShape, stringOrElement: PluggableText) {
if (typeof stringOrElement === 'string') {
// This is the case that we hope for
return stringOrElement;
}
if (stringOrElement.type === FormattedMessage) {
if (!stringOrElement) {
return '';
}
if (typeof stringOrElement === 'object' && 'type' in stringOrElement && stringOrElement.type === FormattedMessage) {
// This is a FormattedMessage, so extract the props to translate the text manually
return intl.formatMessage(
{
@@ -194,13 +198,17 @@ class ChannelHeaderPlug extends React.PureComponent<ChannelHeaderPlugProps, Chan
ariaLabel = tooltipToAriaLabelHack(this.props.intl, plug.dropdownText);
}
// TODO: Remove this any and make sure the types are properly
// handled.
const tooltipText: any = plug.tooltipText ?? plug.dropdownText ?? '';
return (
<HeaderIconWrapper
key={'channelHeaderButton' + plug.id}
buttonClass='channel-header__icon'
onClick={() => this.fireAction(plug.action!)}
buttonId={plug.id + 'ChannelHeaderButton'}
tooltip={plug.tooltipText ?? plug.dropdownText ?? ''}
tooltip={tooltipText}
ariaLabelOverride={ariaLabel}
pluginId={plug.pluginId}
>
@@ -279,7 +287,7 @@ class ChannelHeaderPlug extends React.PureComponent<ChannelHeaderPlugProps, Chan
);
};
createDropdown = (plugs: PluginComponent[], appBindings: AppBinding[]) => {
createDropdown = (plugs: ChannelHeaderButtonAction[], appBindings: AppBinding[]) => {
const componentItems = plugs.filter((plug) => plug.action).map((plug) => {
return (
<li

Просмотреть файл

@@ -14,14 +14,14 @@ import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
import {createCallContext} from 'utils/apps';
import type {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForChannel} from 'types/apps';
import type {PluginComponent} from 'types/store/plugins';
import type {MobileChannelHeaderButtonAction} from 'types/store/plugins';
type Props = {
/*
* Components or actions to add as channel header buttons
*/
components?: PluginComponent[];
components?: MobileChannelHeaderButtonAction[];
/*
* Set to true if the plug is in the dropdown
@@ -84,7 +84,7 @@ class MobileChannelHeaderPlug extends React.PureComponent<Props> {
</li>
);
};
createButton = (plug: PluginComponent) => {
createButton = (plug: MobileChannelHeaderButtonAction) => {
const onClick = () => this.fireAction(plug);
if (this.props.isDropdown) {
@@ -119,7 +119,7 @@ class MobileChannelHeaderPlug extends React.PureComponent<Props> {
);
};
createList(plugs: PluginComponent[]) {
createList(plugs: MobileChannelHeaderButtonAction[]) {
return plugs.map(this.createButton);
}
@@ -127,7 +127,7 @@ class MobileChannelHeaderPlug extends React.PureComponent<Props> {
return bindings.map(this.createAppButton);
}
fireAction(plug: PluginComponent) {
fireAction(plug: MobileChannelHeaderButtonAction) {
return plug.action?.(this.props.channel, this.props.channelMember);
}

Просмотреть файл

@@ -1,692 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`plugins/Pluggable should match snapshot with extended component 1`] = `
<Pluggable
components={
Object {
"AppBar": Array [],
"CallButton": Array [],
"ChannelHeaderButton": Array [],
"CodeBlockAction": Array [],
"CreateBoardFromTemplate": Array [],
"DesktopNotificationHooks": Array [],
"FilePreview": Array [],
"FilesWillUploadHook": Array [],
"LinkTooltip": Array [],
"MainMenu": Array [],
"MessageWillBePosted": Array [],
"MessageWillBeUpdated": Array [],
"MessageWillFormat": Array [],
"MobileChannelHeaderButton": Array [],
"NeedsTeamComponent": Array [],
"NewMessagesSeparatorAction": Array [],
"PopoverSection1": Array [
Object {
"component": [Function],
"id": "",
"pluginId": "",
},
],
"PostAction": Array [],
"PostDropdownMenu": Array [],
"PostEditorAction": Array [],
"Product": Array [],
"RightHandSidebarComponent": Array [],
"SlashCommandWillBePosted": Array [],
"UserGuideDropdownItem": Array [],
}
}
pluggableName="PopoverSection1"
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#162545",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
>
<PluggableErrorBoundary
key="PopoverSection1"
pluginId=""
<div>
<span
data-testid="pluginId"
>
<ProfilePopoverPlugin
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#162545",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
webSocketClient={
WebSocketClient {
"closeCallback": null,
"closeListeners": Set {},
"conn": null,
"connectFailCount": 0,
"connectionId": "",
"connectionUrl": null,
"errorCallback": null,
"errorListeners": Set {},
"eventCallback": null,
"firstConnectCallback": null,
"firstConnectListeners": Set {},
"messageListeners": Set {},
"missedEventCallback": null,
"missedMessageListeners": Set {},
"postedAck": false,
"reconnectCallback": null,
"reconnectListeners": Set {},
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
}
}
>
<span
id="pluginId"
>
ProfilePopoverPlugin
</span>
</ProfilePopoverPlugin>
</PluggableErrorBoundary>
</Pluggable>
ProfilePopoverPlugin
</span>
</div>
`;
exports[`plugins/Pluggable should match snapshot with extended component with pluggableName 1`] = `
<Pluggable
components={
Object {
"AppBar": Array [],
"CallButton": Array [],
"ChannelHeaderButton": Array [],
"CodeBlockAction": Array [],
"CreateBoardFromTemplate": Array [],
"DesktopNotificationHooks": Array [],
"FilePreview": Array [],
"FilesWillUploadHook": Array [],
"LinkTooltip": Array [],
"MainMenu": Array [],
"MessageWillBePosted": Array [],
"MessageWillBeUpdated": Array [],
"MessageWillFormat": Array [],
"MobileChannelHeaderButton": Array [],
"NeedsTeamComponent": Array [],
"NewMessagesSeparatorAction": Array [],
"PopoverSection1": Array [
Object {
"component": [Function],
"id": "",
"pluginId": "",
},
],
"PostAction": Array [],
"PostDropdownMenu": Array [],
"PostEditorAction": Array [],
"Product": Array [],
"RightHandSidebarComponent": Array [],
"SlashCommandWillBePosted": Array [],
"UserGuideDropdownItem": Array [],
}
}
pluggableName="PopoverSection1"
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#162545",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
>
<PluggableErrorBoundary
key="PopoverSection1"
pluginId=""
>
<ProfilePopoverPlugin
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#162545",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
webSocketClient={
WebSocketClient {
"closeCallback": null,
"closeListeners": Set {},
"conn": null,
"connectFailCount": 0,
"connectionId": "",
"connectionUrl": null,
"errorCallback": null,
"errorListeners": Set {},
"eventCallback": null,
"firstConnectCallback": null,
"firstConnectListeners": Set {},
"messageListeners": Set {},
"missedEventCallback": null,
"missedMessageListeners": Set {},
"postedAck": false,
"reconnectCallback": null,
"reconnectListeners": Set {},
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
}
}
>
<span
id="pluginId"
>
ProfilePopoverPlugin
</span>
</ProfilePopoverPlugin>
</PluggableErrorBoundary>
</Pluggable>
`;
exports[`plugins/Pluggable should match snapshot with no extended component 1`] = `
<Pluggable
components={
Object {
"AppBar": Array [],
"CallButton": Array [],
"ChannelHeaderButton": Array [],
"CodeBlockAction": Array [],
"CreateBoardFromTemplate": Array [],
"DesktopNotificationHooks": Array [],
"FilePreview": Array [],
"FilesWillUploadHook": Array [],
"LinkTooltip": Array [],
"MainMenu": Array [],
"MessageWillBePosted": Array [],
"MessageWillBeUpdated": Array [],
"MessageWillFormat": Array [],
"MobileChannelHeaderButton": Array [],
"NeedsTeamComponent": Array [],
"NewMessagesSeparatorAction": Array [],
"PostAction": Array [],
"PostDropdownMenu": Array [],
"PostEditorAction": Array [],
"Product": Array [],
"RightHandSidebarComponent": Array [],
"SlashCommandWillBePosted": Array [],
"UserGuideDropdownItem": Array [],
}
}
pluggableName=""
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#162545",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
/>
`;
exports[`plugins/Pluggable should match snapshot with non-null pluggableId 1`] = `
<Pluggable
components={
Object {
"AppBar": Array [],
"CallButton": Array [],
"ChannelHeaderButton": Array [],
"CodeBlockAction": Array [],
"CreateBoardFromTemplate": Array [],
"DesktopNotificationHooks": Array [],
"FilePreview": Array [],
"FilesWillUploadHook": Array [],
"LinkTooltip": Array [],
"MainMenu": Array [],
"MessageWillBePosted": Array [],
"MessageWillBeUpdated": Array [],
"MessageWillFormat": Array [],
"MobileChannelHeaderButton": Array [],
"NeedsTeamComponent": Array [],
"NewMessagesSeparatorAction": Array [],
"PopoverSection1": Array [
Object {
"component": [Function],
"id": "",
"pluginId": "",
},
],
"PostAction": Array [],
"PostDropdownMenu": Array [],
"PostEditorAction": Array [],
"Product": Array [],
"RightHandSidebarComponent": Array [],
"SlashCommandWillBePosted": Array [],
"UserGuideDropdownItem": Array [],
}
}
pluggableId="pluggableId"
pluggableName="PopoverSection1"
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#162545",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
/>
`;
exports[`plugins/Pluggable should match snapshot with null pluggableId 1`] = `
<Pluggable
components={
Object {
"AppBar": Array [],
"CallButton": Array [],
"ChannelHeaderButton": Array [],
"CodeBlockAction": Array [],
"CreateBoardFromTemplate": Array [],
"DesktopNotificationHooks": Array [],
"FilePreview": Array [],
"FilesWillUploadHook": Array [],
"LinkTooltip": Array [],
"MainMenu": Array [],
"MessageWillBePosted": Array [],
"MessageWillBeUpdated": Array [],
"MessageWillFormat": Array [],
"MobileChannelHeaderButton": Array [],
"NeedsTeamComponent": Array [],
"NewMessagesSeparatorAction": Array [],
"PopoverSection1": Array [
Object {
"component": [Function],
"id": "",
"pluginId": "",
},
],
"PostAction": Array [],
"PostDropdownMenu": Array [],
"PostEditorAction": Array [],
"Product": Array [],
"RightHandSidebarComponent": Array [],
"SlashCommandWillBePosted": Array [],
"UserGuideDropdownItem": Array [],
}
}
pluggableName="PopoverSection1"
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#162545",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
>
<PluggableErrorBoundary
key="PopoverSection1"
pluginId=""
>
<ProfilePopoverPlugin
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#162545",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
webSocketClient={
WebSocketClient {
"closeCallback": null,
"closeListeners": Set {},
"conn": null,
"connectFailCount": 0,
"connectionId": "",
"connectionUrl": null,
"errorCallback": null,
"errorListeners": Set {},
"eventCallback": null,
"firstConnectCallback": null,
"firstConnectListeners": Set {},
"messageListeners": Set {},
"missedEventCallback": null,
"missedMessageListeners": Set {},
"postedAck": false,
"reconnectCallback": null,
"reconnectListeners": Set {},
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
}
}
>
<span
id="pluginId"
>
ProfilePopoverPlugin
</span>
</ProfilePopoverPlugin>
</PluggableErrorBoundary>
</Pluggable>
`;
exports[`plugins/Pluggable should match snapshot with non-null pluggableId 1`] = `<div />`;
exports[`plugins/Pluggable should match snapshot with valid pluggableId 1`] = `
<Pluggable
components={
Object {
"AppBar": Array [],
"CallButton": Array [],
"ChannelHeaderButton": Array [],
"CodeBlockAction": Array [],
"CreateBoardFromTemplate": Array [],
"DesktopNotificationHooks": Array [],
"FilePreview": Array [],
"FilesWillUploadHook": Array [],
"LinkTooltip": Array [],
"MainMenu": Array [],
"MessageWillBePosted": Array [],
"MessageWillBeUpdated": Array [],
"MessageWillFormat": Array [],
"MobileChannelHeaderButton": Array [],
"NeedsTeamComponent": Array [],
"NewMessagesSeparatorAction": Array [],
"PopoverSection1": Array [
Object {
"component": [Function],
"id": "pluggableId",
"pluginId": "",
},
],
"PostAction": Array [],
"PostDropdownMenu": Array [],
"PostEditorAction": Array [],
"Product": Array [],
"RightHandSidebarComponent": Array [],
"SlashCommandWillBePosted": Array [],
"UserGuideDropdownItem": Array [],
}
}
pluggableId="pluggableId"
pluggableName="PopoverSection1"
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#162545",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
>
<PluggableErrorBoundary
key="PopoverSection1pluggableId"
pluginId=""
<div>
<span
data-testid="pluginId"
>
<ProfilePopoverPlugin
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"mentionBg": "#ffffff",
"mentionBj": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#162545",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
}
}
webSocketClient={
WebSocketClient {
"closeCallback": null,
"closeListeners": Set {},
"conn": null,
"connectFailCount": 0,
"connectionId": "",
"connectionUrl": null,
"errorCallback": null,
"errorListeners": Set {},
"eventCallback": null,
"firstConnectCallback": null,
"firstConnectListeners": Set {},
"messageListeners": Set {},
"missedEventCallback": null,
"missedMessageListeners": Set {},
"postedAck": false,
"reconnectCallback": null,
"reconnectListeners": Set {},
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
}
}
>
<span
id="pluginId"
>
ProfilePopoverPlugin
</span>
</ProfilePopoverPlugin>
</PluggableErrorBoundary>
</Pluggable>
ProfilePopoverPlugin
</span>
</div>
`;

Просмотреть файл

@@ -1,19 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import type {GlobalState} from 'types/store';
import Pluggable from './pluggable';
function mapStateToProps(state: GlobalState) {
return {
components: state.plugins.components,
theme: getTheme(state),
};
}
export default connect(mapStateToProps)(Pluggable);
export default Pluggable;

Просмотреть файл

@@ -3,150 +3,116 @@
import type {ComponentProps} from 'react';
import React from 'react';
import {Provider} from 'react-redux';
import {Preferences} from 'mattermost-redux/constants';
import type {DeepPartial} from '@mattermost/types/utilities';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import testConfigureStore from 'tests/test_store';
import Pluggable from './pluggable';
import type {GlobalState} from 'types/store';
class ProfilePopoverPlugin extends React.PureComponent {
render() {
return <span id='pluginId'>{'ProfilePopoverPlugin'}</span>;
}
}
import Pluggable from '.';
const ProfilePopoverPlugin: React.FunctionComponent = () => (<span data-testid='pluginId'>{'ProfilePopoverPlugin'}</span>);
jest.mock('actions/views/profile_popover');
describe('plugins/Pluggable', () => {
const baseProps: ComponentProps<typeof Pluggable> = {
pluggableName: '',
components: {
Product: [],
CallButton: [],
PostDropdownMenu: [],
PostAction: [],
PostEditorAction: [],
CodeBlockAction: [],
NewMessagesSeparatorAction: [],
FilePreview: [],
MainMenu: [],
LinkTooltip: [],
RightHandSidebarComponent: [],
ChannelHeaderButton: [],
MobileChannelHeaderButton: [],
AppBar: [],
UserGuideDropdownItem: [],
FilesWillUploadHook: [],
NeedsTeamComponent: [],
CreateBoardFromTemplate: [],
DesktopNotificationHooks: [],
MessageWillBePosted: [],
MessageWillBeUpdated: [],
MessageWillFormat: [],
SlashCommandWillBePosted: [],
},
theme: Preferences.THEMES.denim,
pluggableName: 'RightHandSidebarComponent',
};
test('should match snapshot with no extended component', () => {
const wrapper = mountWithIntl(
<Pluggable
{...baseProps}
/>,
);
expect(wrapper).toMatchSnapshot();
});
function getBaseState(): DeepPartial<GlobalState> {
return {
plugins: {
components: {
RightHandSidebarComponent: [{
component: ProfilePopoverPlugin,
id: 'some id',
pluginId: 'some plugin id',
}],
},
},
entities: {
teams: {
currentTeamId: '',
},
preferences: {
myPreferences: {},
},
general: {
config: {},
},
},
};
}
test('should match snapshot with extended component', () => {
const wrapper = mountWithIntl(
const state = getBaseState();
const {container} = renderWithContext(
<Pluggable
{...baseProps}
pluggableName='PopoverSection1'
components={{...baseProps.components, PopoverSection1: [{id: '', pluginId: '', component: ProfilePopoverPlugin}]}}
pluggableName='RightHandSidebarComponent'
/>,
state,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('#pluginId').text()).toBe('ProfilePopoverPlugin');
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(true);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('pluginId')).toBeInTheDocument();
expect(screen.getByText('ProfilePopoverPlugin')).toBeInTheDocument();
});
test('should match snapshot with extended component with pluggableName', () => {
const wrapper = mountWithIntl(
<Pluggable
{...baseProps}
pluggableName='PopoverSection1'
components={{...baseProps.components, PopoverSection1: [{id: '', pluginId: '', component: ProfilePopoverPlugin}]}}
/>,
test('should return null if with pluggableName but no components', () => {
const state = getBaseState();
state.plugins!.components!.RightHandSidebarComponent = [];
const store = testConfigureStore(state);
const {container} = renderWithContext(
<Provider store={store}>
<Pluggable
{...baseProps}
pluggableName='RightHandSidebarComponent'
/>
</Provider>,
state,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('#pluginId').text()).toBe('ProfilePopoverPlugin');
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(true);
});
test('should return null if neither pluggableName nor children is is defined in props', () => {
const wrapper = mountWithIntl(
<Pluggable
{...baseProps}
components={{...baseProps.components, PopoverSection1: [{id: '', pluginId: '', component: ProfilePopoverPlugin}]}}
/>,
);
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(false);
});
test('should return null if with pluggableName but no children', () => {
const wrapper = mountWithIntl(
<Pluggable
{...baseProps}
pluggableName='PopoverSection1'
/>,
);
expect(wrapper.children().length).toBe(0);
expect(container).toBeEmptyDOMElement();
});
test('should match snapshot with non-null pluggableId', () => {
const wrapper = mountWithIntl(
<Pluggable
{...baseProps}
pluggableName='PopoverSection1'
pluggableId={'pluggableId'}
components={{...baseProps.components, PopoverSection1: [{id: '', pluginId: '', component: ProfilePopoverPlugin}]}}
/>,
const state = getBaseState();
const store = testConfigureStore(state);
const {container} = renderWithContext(
<Provider store={store}>
<Pluggable
{...baseProps}
pluggableName='RightHandSidebarComponent'
pluggableId={'pluggableId'}
/>
</Provider>,
state,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(false);
});
test('should match snapshot with null pluggableId', () => {
const wrapper = mountWithIntl(
<Pluggable
{...baseProps}
pluggableName='PopoverSection1'
components={{...baseProps.components, PopoverSection1: [{id: '', pluginId: '', component: ProfilePopoverPlugin}]}}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(true);
expect(container).toMatchSnapshot();
expect(container).toBeEmptyDOMElement();
});
test('should match snapshot with valid pluggableId', () => {
const wrapper = mountWithIntl(
<Pluggable
{...baseProps}
pluggableName='PopoverSection1'
pluggableId={'pluggableId'}
components={{...baseProps.components, PopoverSection1: [{id: 'pluggableId', pluginId: '', component: ProfilePopoverPlugin}]}}
/>,
const state = getBaseState();
state.plugins!.components!.RightHandSidebarComponent![0]!.id = 'pluggableId';
const store = testConfigureStore(state);
const {container} = renderWithContext(
<Provider store={store}>
<Pluggable
{...baseProps}
pluggableName='RightHandSidebarComponent'
pluggableId={'pluggableId'}
/>
</Provider>,
state,
);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(true);
expect(container).toMatchSnapshot();
expect(screen.getByTestId('pluginId')).toBeInTheDocument();
});
});

Просмотреть файл

@@ -2,34 +2,29 @@
// See LICENSE.txt for license information.
import React from 'react';
import {useSelector} from 'react-redux';
import type {WebSocketClient} from '@mattermost/client';
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import webSocketClient from 'client/web_websocket_client';
import type {GlobalState} from 'types/store';
import type {ProductComponent} from 'types/store/plugins';
import type {PluginsState, ProductComponent, ProductSubComponentNames} from 'types/store/plugins';
import PluggableErrorBoundary from './error_boundary';
type Props = {
type ComponentProps<
Key extends keyof PluginsState['components'],
SubKey extends ProductSubComponentNames,
> = Key extends 'Product' ?
(PluginsState['components'][Key][number][SubKey] extends React.ComponentType<any> ? React.ComponentProps<PluginsState['components'][Key][number][SubKey]> : never) :
(PluginsState['components'][Key][number] extends {component: React.ComponentType<any>} ? React.ComponentProps<PluginsState['components'][Key][number]['component']> : never);
type WrapperProps<T extends keyof PluginsState['components'], U extends ProductSubComponentNames> = {
/*
* Override the component to be plugged
*/
pluggableName: string;
/*
* Components for overriding provided by plugins
*/
components: GlobalState['plugins']['components'];
/*
* Logged in user's theme
*/
theme: Theme;
pluggableName: T;
/*
* Id of the specific component to be plugged.
@@ -41,35 +36,33 @@ type Props = {
*
* Only supported when pluggableName is "Product".
*/
subComponentName?: 'mainComponent' | 'publicComponent' | 'headerCentreComponent' | 'headerRightComponent';
/*
* Accept any other prop to pass onto the plugin component
*/
[name: string]: any;
subComponentName?: U;
}
type BaseChildProps = {
theme: Theme;
webSocketClient?: WebSocketClient;
}
export type PluggableProps<Key extends keyof PluginsState['components'], SubKey extends ProductSubComponentNames> = WrapperProps<Key, SubKey> & Omit<ComponentProps<Key, SubKey>, keyof WrapperProps<Key, SubKey> | 'theme' | (Key extends 'Product' ? never : 'webSocketClient')>
export default function Pluggable(props: Props): JSX.Element | null {
export default function Pluggable<Key extends keyof PluginsState['components'], SubKey extends ProductSubComponentNames>(props: PluggableProps<Key, SubKey>) {
const {
components,
pluggableId,
pluggableName,
subComponentName = '',
theme,
...otherProps
} = props;
if (!pluggableName || !Object.hasOwn(components, pluggableName)) {
type PluggableType = PluginsState['components'][Key][number];
const theme = useSelector(getTheme);
const allPluginComponents = useSelector((state: GlobalState) => {
const allComponents = state.plugins.components;
if (Object.hasOwn(allComponents, pluggableName)) {
return allComponents[pluggableName] as PluggableType[];
}
return undefined;
});
if (!pluggableName || !allPluginComponents) {
return null;
}
let pluginComponents = components[pluggableName]!;
let pluginComponents: PluggableType[] = [...allPluginComponents];
if (pluggableId) {
pluginComponents = pluginComponents.filter(
(element) => element.id === pluggableId);
@@ -80,12 +73,16 @@ export default function Pluggable(props: Props): JSX.Element | null {
let content;
if (pluggableName === 'Product') {
content = (pluginComponents as ProductComponent[]).map((pc) => {
const productComponents = pluginComponents as ProductComponent[];
content = (productComponents).map((pc) => {
if (!subComponentName || !pc[subComponentName]) {
return null;
}
const Component = pc[subComponentName]! as React.ComponentType<BaseChildProps>;
// The function arguments typing makes sure the passed props are
// correct, so it is safe to cast here.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Component = pc[subComponentName] as React.ComponentType<any>;
return (
<PluggableErrorBoundary
@@ -101,11 +98,14 @@ export default function Pluggable(props: Props): JSX.Element | null {
});
} else {
content = pluginComponents.map((p) => {
if (!p.component) {
if (!('component' in p) || !p.component) {
return null;
}
const Component = p.component as React.ComponentType<BaseChildProps>;
// The function arguments typing makes sure the passed props are
// correct, so it is safe to cast here.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Component = p.component as React.ComponentType<any>;
return (
<PluggableErrorBoundary
@@ -128,3 +128,5 @@ export default function Pluggable(props: Props): JSX.Element | null {
</>
);
}
export type PluggableComponentType = typeof Pluggable;

Просмотреть файл

@@ -5,10 +5,6 @@ import React from 'react';
import {isValidElementType} from 'react-is';
import type {Reducer} from 'redux';
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
import type {FileInfo} from '@mattermost/types/files';
import type {ProductScope} from '@mattermost/types/products';
import reducerRegistry from 'mattermost-redux/store/reducer_registry';
import {
@@ -35,13 +31,43 @@ import {ActionTypes} from 'utils/constants';
import {reArg} from 'utils/func';
import {generateId} from 'utils/utils';
import type {GlobalState} from 'types/store';
import type {PluginComponent, PluginsState, ProductComponent, NeedsTeamComponent} from 'types/store/plugins';
import type {
PluginsState,
ProductComponent,
NeedsTeamComponent,
PostDropdownMenuAction,
ChannelHeaderAction,
ChannelHeaderButtonAction,
RightHandSidebarComponent,
AppBarAction,
FileUploadMethodAction,
MainMenuAction,
ChannelIntroButtonAction,
UserGuideDropdownAction,
FilesDropdownAction,
CustomRouteComponent,
AdminConsolePluginCustomSection,
AdminConsolePluginComponent,
SearchButtonsComponent,
SearchSuggestionsComponent,
SearchHintsComponent,
CallButtonAction,
CreateBoardFromTemplateComponent,
PostWillRenderEmbedComponent,
FilesWillUploadHook,
MessageWillBePostedHook,
SlashCommandWillBePostedHook,
MessageWillFormatHook,
FilePreviewComponent,
MessageWillBeUpdatedHook,
AppBarChannelAction,
DesktopNotificationHook,
} from 'types/store/plugins';
const defaultShouldRender = () => true;
type DPluginComponentProp = {component: PluginComponent['component']};
function dispatchPluginComponentAction(name: keyof PluginsState['components'], pluginId: string, component: PluginComponent['component'], id = generateId()) {
type DPluginComponentProp = {component: React.ComponentType<unknown>};
function dispatchPluginComponentAction(name: keyof PluginsState['components'], pluginId: string, component: React.ComponentType<any>, id = generateId()) {
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name,
@@ -55,6 +81,14 @@ function dispatchPluginComponentAction(name: keyof PluginsState['components'], p
return id;
}
function dispatchPluginComponentWithData<T extends keyof PluginsState['components']>(name: T, data: PluginsState['components'][T][number]) {
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name,
data,
});
}
type ReactResolvable = React.ReactNode | React.ElementType;
const resolveReactElement = (element: ReactResolvable) => {
if (
@@ -128,17 +162,23 @@ export default class PluginRegistry {
// Register components for search.
// Accepts React components. Returns a unique identifier.
registerSearchComponents = ({buttonComponent, suggestionsComponent, hintsComponent, action}: any) => {
registerSearchComponents = ({
buttonComponent,
suggestionsComponent,
hintsComponent,
action,
}: {
buttonComponent: SearchButtonsComponent['component'];
suggestionsComponent: SearchSuggestionsComponent['component'];
hintsComponent: SearchHintsComponent['component'];
action: SearchButtonsComponent['action'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'SearchButtons',
data: {
id,
pluginId: this.id,
component: buttonComponent,
action,
},
dispatchPluginComponentWithData('SearchButtons', {
id,
pluginId: this.id,
component: buttonComponent,
action,
});
dispatchPluginComponentAction('SearchSuggestions', this.id, suggestionsComponent, id);
dispatchPluginComponentAction('SearchHints', this.id, hintsComponent, id);
@@ -157,20 +197,20 @@ export default class PluginRegistry {
// Register a component fixed to the bottom of the create new channel modal and also registers a callback function to be called after
// the channel has been succesfully created
// Accepts a React component. Returns a unique identifier.
registerActionAfterChannelCreation = reArg(['component', 'action'], ({component, action}) => {
registerActionAfterChannelCreation = reArg(['component', 'action'], ({
component,
action,
}: {
component: CreateBoardFromTemplateComponent['component'];
action: CreateBoardFromTemplateComponent['action'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'CreateBoardFromTemplate',
data: {
id,
pluginId: this.id,
component,
action,
},
dispatchPluginComponentWithData('CreateBoardFromTemplate', {
id,
pluginId: this.id,
component,
action,
});
return id;
});
@@ -193,7 +233,7 @@ export default class PluginRegistry {
tooltipText,
}: {
icon: ReactResolvable;
action: PluginComponent['action'];
action: ChannelHeaderButtonAction['action'];
dropdownText: ReactResolvable;
tooltipText: ReactResolvable;
}) => {
@@ -208,17 +248,8 @@ export default class PluginRegistry {
tooltipText: resolveReactElement(tooltipText),
};
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'ChannelHeaderButton',
data,
});
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'MobileChannelHeaderButton',
data,
});
dispatchPluginComponentWithData('ChannelHeaderButton', data);
dispatchPluginComponentWithData('MobileChannelHeaderButton', data);
return id;
});
@@ -238,7 +269,7 @@ export default class PluginRegistry {
text,
}: {
icon: ReactResolvable;
action: PluginComponent['action'];
action: ChannelIntroButtonAction['action'];
text: ReactResolvable;
}) => {
const id = generateId();
@@ -251,11 +282,7 @@ export default class PluginRegistry {
text,
};
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'ChannelIntroButton',
data,
});
dispatchPluginComponentWithData('ChannelIntroButton', data);
return id;
});
@@ -279,7 +306,7 @@ export default class PluginRegistry {
}: {
button: ReactResolvable;
dropdownButton: ReactResolvable;
action: (currentChannel: Channel, myCurrentChannelMembership: ChannelMembership) => void;
action: CallButtonAction['action'];
}) => {
const id = generateId();
@@ -288,20 +315,12 @@ export default class PluginRegistry {
pluginId: this.id,
button: resolveReactElement(button),
dropdownButton: resolveReactElement(dropdownButton),
icon: null, // Needed to satisfy types for MobileChannelHeaderButton
action,
};
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'CallButton',
data,
});
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'MobileChannelHeaderButton',
data,
});
dispatchPluginComponentWithData('CallButton', data);
dispatchPluginComponentWithData('MobileChannelHeaderButton', data);
return id;
});
@@ -357,19 +376,23 @@ export default class PluginRegistry {
// - component - The component that renders the embed view for the link
// - toggleable - A boolean indicating if the embed view should be collapsable
// Returns a unique identifier.
registerPostWillRenderEmbedComponent = reArg(['match', 'component', 'toggleable'], ({match, component, toggleable}) => {
registerPostWillRenderEmbedComponent = reArg(['match', 'component', 'toggleable'], ({
match,
component,
toggleable,
}: {
match: PostWillRenderEmbedComponent['match'];
component: PostWillRenderEmbedComponent['component'];
toggleable: PostWillRenderEmbedComponent['toggleable'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'PostWillRenderEmbedComponent',
data: {
id,
pluginId: this.id,
component,
match,
toggleable,
},
dispatchPluginComponentWithData('PostWillRenderEmbedComponent', {
id,
pluginId: this.id,
component,
match,
toggleable,
});
return id;
@@ -391,21 +414,17 @@ export default class PluginRegistry {
mobileIcon,
}: {
text: ReactResolvable;
action: PluginComponent['action'];
action: MainMenuAction['action'];
mobileIcon: ReactResolvable;
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'MainMenu',
data: {
id,
pluginId: this.id,
text: resolveReactElement(text),
action,
mobileIcon: resolveReactElement(mobileIcon),
},
dispatchPluginComponentWithData('MainMenu', {
id,
pluginId: this.id,
text: resolveReactElement(text),
action,
mobileIcon: resolveReactElement(mobileIcon),
});
return id;
@@ -427,22 +446,18 @@ export default class PluginRegistry {
action,
shouldRender = defaultShouldRender,
}: {
text: ReactResolvable;
action: PluginComponent['action'];
shouldRender?: (state: GlobalState) => boolean;
text: ChannelHeaderAction['text'];
action: ChannelHeaderAction['action'];
shouldRender?: ChannelHeaderAction['shouldRender'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'ChannelHeader',
data: {
id,
pluginId: this.id,
text: resolveReactElement(text),
action,
shouldRender,
},
dispatchPluginComponentWithData('ChannelHeader', {
id,
pluginId: this.id,
text: resolveReactElement(text),
action,
shouldRender,
});
return id;
@@ -463,22 +478,18 @@ export default class PluginRegistry {
text,
action,
}: {
match: (fileInfo: FileInfo) => boolean;
match: FilesDropdownAction['match'];
text: ReactResolvable;
action: PluginComponent['action'];
action: FilesDropdownAction['action'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'FilesDropdown',
data: {
id,
pluginId: this.id,
match,
text: resolveReactElement(text),
action,
},
dispatchPluginComponentWithData('FilesDropdown', {
id,
pluginId: this.id,
match,
text: resolveReactElement(text),
action,
});
return id;
@@ -497,19 +508,15 @@ export default class PluginRegistry {
action,
}: {
text: ReactResolvable;
action: PluginComponent['action'];
action: UserGuideDropdownAction['action'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'UserGuideDropdown',
data: {
id,
pluginId: this.id,
text: resolveReactElement(text),
action,
},
dispatchPluginComponentWithData('UserGuideDropdown', {
id,
pluginId: this.id,
text: resolveReactElement(text),
action,
});
return id;
@@ -554,22 +561,18 @@ export default class PluginRegistry {
action,
filter,
}: {
text: ReactResolvable;
action: PluginComponent['action'];
filter: PluginComponent['filter'];
text: PostDropdownMenuAction['text'];
action: PostDropdownMenuAction['action'];
filter: PostDropdownMenuAction['filter'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'PostDropdownMenu',
data: {
id,
pluginId: this.id,
text: resolveReactElement(text),
action,
filter,
},
dispatchPluginComponentWithData('PostDropdownMenu', {
id,
pluginId: this.id,
text: resolveReactElement(text),
action,
filter,
});
return id;
@@ -593,37 +596,33 @@ export default class PluginRegistry {
filter,
}: {
text: ReactResolvable;
action: PluginComponent['action'];
filter: PluginComponent['filter'];
action: PostDropdownMenuAction['action'];
filter: PostDropdownMenuAction['filter'];
}) => {
const id = generateId();
const registerMenuItem = (
pluginId: string,
id: string,
parentMenuId: string | null,
parentMenuId: string | undefined,
innerText: ReactResolvable,
innerAction: PluginComponent['action'],
innerFilter: PluginComponent['filter'],
innerAction: PostDropdownMenuAction['action'],
innerFilter: PostDropdownMenuAction['filter'],
) => {
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'PostDropdownMenu',
data: {
id,
parentMenuId,
pluginId,
text: resolveReactElement(innerText),
subMenu: [],
action: innerAction,
filter: innerFilter,
},
dispatchPluginComponentWithData('PostDropdownMenu', {
id,
parentMenuId,
pluginId,
text: resolveReactElement(innerText),
subMenu: [],
action: innerAction,
filter: innerFilter,
});
type TInnerParams = [
innerText: ReactResolvable,
innerAction: PluginComponent['action'],
innerFilter: PluginComponent['filter'],
innerAction: PostDropdownMenuAction['action'],
innerFilter: PostDropdownMenuAction['filter'],
];
return function registerSubMenuItem(...args: TInnerParams) {
@@ -635,7 +634,7 @@ export default class PluginRegistry {
};
};
return {id, rootRegisterMenuItem: registerMenuItem(this.id, id, null, text, action, filter)};
return {id, rootRegisterMenuItem: registerMenuItem(this.id, id, undefined, text, action, filter)};
});
// Register a component at the bottom of the post dropdown menu.
@@ -660,21 +659,17 @@ export default class PluginRegistry {
text,
}: {
icon: ReactResolvable;
action: PluginComponent['action'];
action: FileUploadMethodAction['action'];
text: ReactResolvable;
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'FileUploadMethod',
data: {
id,
pluginId: this.id,
text,
action,
icon,
},
dispatchPluginComponentWithData('FileUploadMethod', {
id,
pluginId: this.id,
text,
action,
icon,
});
return id;
@@ -687,17 +682,15 @@ export default class PluginRegistry {
// - message - An error message to display, leave blank or null to display no message
// - files - Modified array of files to upload, set to null to reject all files
// Returns a unique identifier.
registerFilesWillUploadHook = reArg(['hook'], ({hook}) => {
registerFilesWillUploadHook = reArg(['hook'], ({hook}: {
hook: FilesWillUploadHook['hook'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'FilesWillUploadHook',
data: {
id,
pluginId: this.id,
hook,
},
dispatchPluginComponentWithData('FilesWillUploadHook', {
id,
pluginId: this.id,
hook,
});
return id;
@@ -771,17 +764,15 @@ export default class PluginRegistry {
//
// If the hook function is asynchronous, the message will not be sent to the server
// until the hook returns.
registerMessageWillBePostedHook = reArg(['hook'], ({hook}) => {
registerMessageWillBePostedHook = reArg(['hook'], ({hook}: {
hook: MessageWillBePostedHook['hook'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'MessageWillBePosted',
data: {
id,
pluginId: this.id,
hook,
},
dispatchPluginComponentWithData('MessageWillBePosted', {
id,
pluginId: this.id,
hook,
});
return id;
@@ -807,17 +798,15 @@ export default class PluginRegistry {
//
// If the hook function is asynchronous, the command will not be sent to the server
// until the hook returns.
registerSlashCommandWillBePostedHook = reArg(['hook'], ({hook}) => {
registerSlashCommandWillBePostedHook = reArg(['hook'], ({hook}: {
hook: SlashCommandWillBePostedHook['hook'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'SlashCommandWillBePosted',
data: {
id,
pluginId: this.id,
hook,
},
dispatchPluginComponentWithData('SlashCommandWillBePosted', {
id,
pluginId: this.id,
hook,
});
return id;
@@ -828,17 +817,15 @@ export default class PluginRegistry {
// already modified by other hooks) as arguments. This function must return a string
// message that will be formatted.
// Returns a unique identifier.
registerMessageWillFormatHook = reArg(['hook'], ({hook}) => {
registerMessageWillFormatHook = reArg(['hook'], ({hook}: {
hook: MessageWillFormatHook['hook'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'MessageWillFormat',
data: {
id,
pluginId: this.id,
hook,
},
dispatchPluginComponentWithData('MessageWillFormat', {
id,
pluginId: this.id,
hook,
});
return id;
@@ -851,18 +838,17 @@ export default class PluginRegistry {
// - component - A react component to display instead of original preview. Receives fileInfo and post as props.
// Returns a unique identifier.
// Only one plugin can override a file preview at a time. If two plugins try to override the same file preview, the first plugin will perform the override and the second will not. Plugin precedence is ordered alphabetically by plugin ID.
registerFilePreviewComponent = reArg(['override', 'component'], ({override, component}) => {
registerFilePreviewComponent = reArg(['override', 'component'], ({override, component}: {
override: FilePreviewComponent['override'];
component: FilePreviewComponent['component'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'FilePreview',
data: {
id,
pluginId: this.id,
override,
component,
},
dispatchPluginComponentWithData('FilePreview', {
id,
pluginId: this.id,
override,
component,
});
return id;
@@ -908,7 +894,7 @@ export default class PluginRegistry {
options: {showTitle} = {showTitle: false},
}: {
key: string;
component: PluginComponent['component'];
component: AdminConsolePluginComponent['component'];
options?: {showTitle: boolean};
}) => {
store.dispatch(registerAdminConsoleCustomSetting(this.id, key, component, {showTitle}));
@@ -926,7 +912,7 @@ export default class PluginRegistry {
component,
}: {
key: string;
component: PluginComponent['component'];
component: AdminConsolePluginCustomSection['component'];
}) => {
store.dispatch(registerAdminConsoleCustomSection(this.id, key, component));
});
@@ -940,18 +926,23 @@ export default class PluginRegistry {
// - showRHSPlugin: the action to dispatch that will open the RHS.
// - hideRHSPlugin: the action to dispatch that will close the RHS
// - toggleRHSPlugin: the action to dispatch that will toggle the RHS
registerRightHandSidebarComponent = reArg(['component', 'title'], ({component, title}: {component: PluginComponent['component']; title: ReactResolvable}) => {
registerRightHandSidebarComponent = reArg([
'component',
'title',
], ({
component,
title,
}: {
component: RightHandSidebarComponent['component'];
title: ReactResolvable;
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'RightHandSidebarComponent',
data: {
id,
pluginId: this.id,
component,
title: resolveReactElement(title),
},
dispatchPluginComponentWithData('RightHandSidebarComponent', {
id,
pluginId: this.id,
component,
title: resolveReactElement(title),
});
return {id, showRHSPlugin: showRHSPlugin(id), hideRHSPlugin: hideRHSPlugin(id), toggleRHSPlugin: toggleRHSPlugin(id)};
@@ -977,15 +968,11 @@ export default class PluginRegistry {
let fixedRoute = standardizeRoute(route);
fixedRoute = this.id + '/' + fixedRoute;
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'NeedsTeamComponent',
data: {
id,
pluginId: this.id,
component,
route: fixedRoute,
},
dispatchPluginComponentWithData('NeedsTeamComponent', {
id,
pluginId: this.id,
component,
route: fixedRoute,
});
return id;
@@ -1007,21 +994,17 @@ export default class PluginRegistry {
component,
}: {
route: string;
component: PluginComponent['component'];
component: CustomRouteComponent['component'];
}) => {
const id = generateId();
let fixedRoute = standardizeRoute(route);
fixedRoute = this.id + '/' + fixedRoute;
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'CustomRouteComponent',
data: {
id,
pluginId: this.id,
component,
route: fixedRoute,
},
dispatchPluginComponentWithData('CustomRouteComponent', {
id,
pluginId: this.id,
component,
route: fixedRoute,
});
return id;
@@ -1061,24 +1044,20 @@ export default class PluginRegistry {
}: Omit<ProductComponent, 'id' | 'pluginId'>) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'Product',
data: {
id,
pluginId: this.id,
switcherIcon,
switcherText: resolveReactElement(switcherText),
baseURL: '/' + standardizeRoute(baseURL),
switcherLinkURL: '/' + standardizeRoute(switcherLinkURL),
mainComponent,
headerCentreComponent,
headerRightComponent,
showTeamSidebar,
showAppBar,
wrapped,
publicComponent,
},
dispatchPluginComponentWithData('Product', {
id,
pluginId: this.id,
switcherIcon,
switcherText: resolveReactElement(switcherText),
baseURL: '/' + standardizeRoute(baseURL),
switcherLinkURL: '/' + standardizeRoute(switcherLinkURL),
mainComponent,
headerCentreComponent,
headerRightComponent,
showTeamSidebar,
showAppBar,
wrapped,
publicComponent,
});
return id;
@@ -1095,17 +1074,15 @@ export default class PluginRegistry {
//
// If the hook function is asynchronous, the message will not be sent to the server
// until the hook returns.
registerMessageWillBeUpdatedHook = reArg(['hook'], ({hook}) => {
registerMessageWillBeUpdatedHook = reArg(['hook'], ({hook}: {
hook: MessageWillBeUpdatedHook['hook'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'MessageWillBeUpdated',
data: {
id,
pluginId: this.id,
hook,
},
dispatchPluginComponentWithData('MessageWillBeUpdated', {
id,
pluginId: this.id,
hook,
});
return id;
@@ -1162,37 +1139,33 @@ export default class PluginRegistry {
rhsComponent,
rhsTitle,
}: {
iconUrl: string;
iconUrl: AppBarAction['iconUrl'];
tooltipText: ReactResolvable;
supportedProductIds: ProductScope;
supportedProductIds: AppBarAction['supportedProductIds'];
} & ({
action: PluginComponent['action'];
action: AppBarChannelAction;
rhsComponent?: never;
rhsTitle?: never;
} | {
action?: never;
rhsComponent: PluginComponent;
rhsComponent: RightHandSidebarComponent['component'];
rhsTitle: ReactResolvable;
})) => {
const id = generateId();
const registeredRhsComponent = rhsComponent && this.registerRightHandSidebarComponent({title: rhsTitle, component: rhsComponent});
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'AppBar',
data: {
id,
pluginId: this.id,
iconUrl,
tooltipText: resolveReactElement(tooltipText),
supportedProductIds,
...registeredRhsComponent ? {
action: () => store.dispatch(registeredRhsComponent.toggleRHSPlugin),
rhsComponentId: registeredRhsComponent.id,
} : {
action,
},
dispatchPluginComponentWithData('AppBar', {
id,
pluginId: this.id,
iconUrl,
tooltipText: resolveReactElement(tooltipText),
supportedProductIds,
...registeredRhsComponent ? {
action: () => store.dispatch(registeredRhsComponent.toggleRHSPlugin),
rhsComponentId: registeredRhsComponent.id,
} : {
action: action!,
},
});
@@ -1247,17 +1220,15 @@ export default class PluginRegistry {
// completed. The resulting args will be used as the arguments for the `notifyMe` function.
//
// Returns a unique identifier.
registerDesktopNotificationHook = reArg(['hook'], ({hook}) => {
registerDesktopNotificationHook = reArg(['hook'], ({hook}: {
hook: DesktopNotificationHook['hook'];
}) => {
const id = generateId();
store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'DesktopNotificationHooks',
data: {
id,
pluginId: this.id,
hook,
},
dispatchPluginComponentWithData('DesktopNotificationHooks', {
id,
pluginId: this.id,
hook,
});
return id;

Просмотреть файл

@@ -6,14 +6,13 @@ import {connect} from 'react-redux';
import {getPluggableId} from 'selectors/rhs';
import type {GlobalState} from 'types/store';
import type {PluginComponent} from 'types/store/plugins';
import RHSPlugin from './rhs_plugin';
function mapStateToProps(state: GlobalState) {
const rhsPlugins: PluginComponent[] = state.plugins.components.RightHandSidebarComponent;
const rhsPlugins = state.plugins.components.RightHandSidebarComponent;
const pluggableId = getPluggableId(state);
const pluginComponent = rhsPlugins.find((element: PluginComponent) => element.id === pluggableId);
const pluginComponent = rhsPlugins.find((element) => element.id === pluggableId);
const pluginTitle = pluginComponent ? pluginComponent.title : '';
return {

Просмотреть файл

@@ -345,6 +345,11 @@ exports[`plugins/MainMenuActions should match snapshot in mobile view with some
</Memo(MenuGroup)>
<Memo(MenuGroup)>
<MenuItemAction
icon={
<i
className="fa fa-anchor"
/>
}
id="someplugin_pluginmenuitem"
key="someplugin_pluginmenuitem"
onClick={[Function]}

Просмотреть файл

@@ -3,16 +3,6 @@
exports[`plugins/PostMessageView should match snapshot with extended post type 1`] = `
<PostMessageView
currentRelativeTeamUrl="team_url"
currentUser={
Object {
"username": "username",
}
}
emojis={
Object {
"name": "smile",
}
}
enableFormatting={true}
isRHS={false}
options={Object {}}
@@ -20,6 +10,9 @@ exports[`plugins/PostMessageView should match snapshot with extended post type 1
Object {
"testtype": Object {
"component": [Function],
"id": "some id",
"pluginId": "some plugin id",
"type": "",
},
}
}
@@ -30,11 +23,6 @@ exports[`plugins/PostMessageView should match snapshot with extended post type 1
"type": "testtype",
}
}
team={
Object {
"name": "team_name",
}
}
theme={
Object {
"awayIndicator": "#ffbc1f",
@@ -145,7 +133,7 @@ exports[`plugins/PostMessageView should match snapshot with no extended post typ
}
/>
</div>
<Connect(Pluggable)
<Pluggable
onHeightChange={[Function]}
pluggableName="PostMessageAttachment"
postId="post_id"

Просмотреть файл

@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ComponentProps} from 'react';
import React from 'react';
import type {UserProfile} from '@mattermost/types/users';
@@ -12,49 +13,37 @@ import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
describe('plugins/MainMenuActions', () => {
const pluginAction = jest.fn();
const requiredProps = {
const requiredProps: ComponentProps<typeof MainMenu> = {
teamId: 'someteamid',
teamType: '',
teamDisplayName: 'some name',
teamName: 'somename',
currentUser: {id: 'someuserid', roles: 'system_user'} as UserProfile,
enableCommands: true,
enableCustomEmoji: true,
enableIncomingWebhooks: true,
enableOutgoingWebhooks: true,
enableOAuthServiceProvider: true,
canManageSystemBots: true,
enableUserCreation: true,
enableEmailInvitations: false,
enablePluginMarketplace: true,
showDropdown: true,
onToggleDropdown: () => {}, //eslint-disable-line no-empty-function
pluginMenuItems: [{id: 'someplugin', pluginId: 'test', text: 'some plugin text', action: pluginAction}],
canCreateOrDeleteCustomEmoji: true,
pluginMenuItems: [{
id: 'someplugin',
pluginId: 'test',
text: 'some plugin text',
action: pluginAction,
mobileIcon: <i className='fa fa-anchor'/>,
}],
canManageIntegrations: true,
moreTeamsToJoin: true,
guestAccessEnabled: true,
teamIsGroupConstrained: true,
teamUrl: '/team',
location: {
pathname: '/team',
},
actions: {
openModal: jest.fn(),
showMentions: jest.fn(),
showFlaggedPosts: jest.fn(),
closeRightHandSide: jest.fn(),
closeRhsMenu: jest.fn(),
getCloudLimits: jest.fn(),
},
isCloud: false,
isStarterFree: false,
subscription: {},
userIsAdmin: true,
isFirstAdmin: false,
canInviteTeamMember: false,
isFreeTrial: false,
teamsLimitReached: false,
usageDeltaTeams: -1,
mobile: false,
};

Просмотреть файл

@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import {shallow, mount} from 'enzyme';
import type {ComponentProps} from 'react';
import React from 'react';
import {Preferences} from 'mattermost-redux/constants';
@@ -14,16 +15,17 @@ const PostTypePlugin = () => (
describe('plugins/PostMessageView', () => {
const post = {type: 'testtype', message: 'this is some text', id: 'post_id'} as any;
const pluginPostTypes = {
testtype: {component: PostTypePlugin},
};
const requiredProps = {
const requiredProps: ComponentProps<typeof PostMessageView> = {
post,
pluginPostTypes,
currentUser: {username: 'username'},
team: {name: 'team_name'},
emojis: {name: 'smile'},
pluginPostTypes: {
testtype: {
id: 'some id',
pluginId: 'some plugin id',
component: PostTypePlugin,
type: '',
},
},
theme: Preferences.THEMES.denim,
enableFormatting: true,
currentRelativeTeamUrl: 'team_url',

Просмотреть файл

@@ -16,13 +16,12 @@ import {extractPluginConfiguration} from 'utils/plugins/plugin_setting_extractio
import type {MMAction} from 'types/store';
import type {
PluginsState,
PluginComponent,
AdminConsolePluginComponent,
AdminConsolePluginCustomSection,
Menu,
PostDropdownMenuAction,
} from 'types/store/plugins';
function hasMenuId(menu: Menu|PluginComponent, menuId: string) {
function hasMenuId(menu: PostDropdownMenuAction, menuId: string) {
if (!menu.subMenu) {
return false;
}
@@ -39,20 +38,20 @@ function hasMenuId(menu: Menu|PluginComponent, menuId: string) {
return false;
}
function buildMenu(rootMenu: Menu|PluginComponent, data: Menu): Menu|PluginComponent {
function buildMenu(rootMenu: PostDropdownMenuAction, data: PostDropdownMenuAction): PostDropdownMenuAction {
// Recursively build the full menu tree.
const subMenu = rootMenu.subMenu?.map((m: Menu) => buildMenu(m, data));
const subMenu = rootMenu.subMenu?.map((m) => buildMenu(m, data));
if (rootMenu.id === data.parentMenuId) {
subMenu?.push(data);
}
return {
...rootMenu,
subMenu: subMenu as Menu[],
subMenu,
};
}
function sortComponents(a: PluginComponent, b: PluginComponent) {
function sortComponents(a: {pluginId: string}, b: {pluginId: string}) {
if (a.pluginId < b.pluginId) {
return -1;
}
@@ -106,7 +105,7 @@ function removePluginComponents(state: PluginsState['components'], action: AnyAc
}
const nextState = {...state};
const types = Object.keys(nextState);
const types = Object.keys(nextState) as Array<keyof PluginsState['components']>;
let modified = false;
for (let i = 0; i < types.length; i++) {
const componentType = types[i];
@@ -115,7 +114,7 @@ function removePluginComponents(state: PluginsState['components'], action: AnyAc
if (componentList[j].pluginId === action.data.id) {
const nextArray = [...nextState[componentType]];
nextArray.splice(j, 1);
nextState[componentType] = nextArray;
nextState[componentType] = nextArray as any;
modified = true;
}
}
@@ -130,7 +129,7 @@ function removePluginComponents(state: PluginsState['components'], action: AnyAc
function removePluginComponent(state: PluginsState['components'], action: AnyAction) {
let newState = state;
const types = Object.keys(state);
const types = Object.keys(state) as Array<keyof PluginsState['components']>;
for (let i = 0; i < types.length; i++) {
const componentType = types[i];
const componentList = state[componentType] || [];
@@ -195,14 +194,33 @@ const initialComponents: PluginsState['components'] = {
NewMessagesSeparatorAction: [],
Product: [],
RightHandSidebarComponent: [],
UserGuideDropdownItem: [],
FilesWillUploadHook: [],
NeedsTeamComponent: [],
CreateBoardFromTemplate: [],
DesktopNotificationHooks: [],
BottomTeamSidebar: [],
ChannelHeader: [],
ChannelIntroButton: [],
CustomRouteComponent: [],
FilesDropdown: [],
FileUploadMethod: [],
LeftSidebarHeader: [],
MessageWillFormat: [],
PopoverUserActions: [],
PopoverUserAttributes: [],
PostDropdownMenuItem: [],
PostMessageAttachment: [],
PostWillRenderEmbedComponent: [],
Root: [],
SearchButtons: [],
SearchHints: [],
SearchSuggestions: [],
UserGuideDropdown: [],
ChannelToast: [],
Global: [],
SidebarChannelLinkLabel: [],
MessageWillBePosted: [],
MessageWillBeUpdated: [],
MessageWillFormat: [],
SlashCommandWillBePosted: [],
};
@@ -210,13 +228,14 @@ function components(state: PluginsState['components'] = initialComponents, actio
switch (action.type) {
case ActionTypes.RECEIVED_PLUGIN_COMPONENT: {
if (action.name && action.data) {
const pluggableType = action.name as keyof PluginsState['components'];
const nextState = {...state};
const currentArray = nextState[action.name] || [];
const currentArray = nextState[pluggableType] || [];
const nextArray = [...currentArray];
let actionData = action.data;
if (action.name === 'PostDropdownMenu' && actionData.parentMenuId) {
// Remove the menu from nextArray to rebuild it later.
const menu = remove(nextArray, (c) => hasMenuId(c, actionData.parentMenuId) && c.pluginId === actionData.pluginId);
const menu = remove(nextArray as PostDropdownMenuAction[], (c) => hasMenuId(c, actionData.parentMenuId) && c.pluginId === actionData.pluginId);
// Request is for an unknown menuId, return original state.
if (!menu[0]) {
@@ -226,7 +245,7 @@ function components(state: PluginsState['components'] = initialComponents, actio
}
nextArray.push(actionData);
nextArray.sort(sortComponents);
nextState[action.name] = nextArray;
nextState[pluggableType] = nextArray as any;
return nextState;
}
return state;

Просмотреть файл

@@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {AppBinding} from '@mattermost/types/apps';
import {Preferences} from 'mattermost-redux/constants';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {appBarEnabled, getAppBarAppBindings} from 'mattermost-redux/selectors/entities/apps';
@@ -11,7 +9,6 @@ import {get} from 'mattermost-redux/selectors/entities/preferences';
import {createShallowSelector} from 'mattermost-redux/utils/helpers';
import type {GlobalState} from 'types/store';
import type {FileDropdownPluginComponent, PluginComponent} from 'types/store/plugins';
export const getPluginUserSettings = createSelector(
'getPluginUserSettings',
@@ -25,7 +22,7 @@ export const getFilesDropdownPluginMenuItems = createSelector(
'getFilesDropdownPluginMenuItems',
(state: GlobalState) => state.plugins.components.FilesDropdown,
(components) => {
return (components || []) as unknown as FileDropdownPluginComponent[];
return (components || []);
},
);
@@ -44,7 +41,7 @@ export const getChannelHeaderPluginComponents = createSelector(
(state: GlobalState) => state.plugins.components.AppBar,
(enabled, channelHeaderComponents = [], appBarComponents = []) => {
if (!enabled || !appBarComponents.length) {
return channelHeaderComponents as unknown as PluginComponent[];
return channelHeaderComponents;
}
// Remove channel header icons for plugins that have also registered an app bar component
@@ -99,7 +96,7 @@ export const shouldShowAppBar = createSelector(
getAppBarAppBindings,
getAppBarPluginComponents,
getChannelHeaderPluginComponents,
(enabled: boolean, bindings: AppBinding[], appBarComponents: PluginComponent[], channelHeaderComponents) => {
(enabled, bindings, appBarComponents, channelHeaderComponents) => {
return enabled && Boolean(bindings.length || appBarComponents.length || channelHeaderComponents.length);
},
);

Просмотреть файл

@@ -2,17 +2,23 @@
// See LICENSE.txt for license information.
import type React from 'react';
import type {RouteComponentProps} from 'react-router-dom';
import type {WebSocketClient} from '@mattermost/client';
import type {IconGlyphTypes} from '@mattermost/compass-icons/IconGlyphs';
import type {PluginAnalyticsRow} from '@mattermost/types/admin';
import type {Channel} from '@mattermost/types/channels';
import type {Board} from '@mattermost/types/boards';
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
import type {FileInfo} from '@mattermost/types/files';
import type {CommandArgs} from '@mattermost/types/integrations';
import type {ClientPluginManifest} from '@mattermost/types/plugins';
import type {Post, PostEmbed} from '@mattermost/types/posts';
import type {ProductScope} from '@mattermost/types/products';
import type {UserProfile} from '@mattermost/types/users';
import type {IDMappedObjects} from '@mattermost/types/utilities';
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
import type {NewPostMessageProps} from 'actions/new_post';
import type {PluginConfiguration} from 'types/plugins/user_settings';
@@ -24,30 +30,48 @@ export type PluginsState = {
plugins: IDMappedObjects<ClientPluginManifest>;
components: {
[componentName: string]: PluginComponent[];
CallButton: CallButtonAction[];
PostDropdownMenu: PostDropdownMenuAction[];
MainMenu: MainMenuAction[];
ChannelHeader: ChannelHeaderAction[];
ChannelHeaderButton: ChannelHeaderButtonAction[];
MobileChannelHeaderButton: MobileChannelHeaderButtonAction[];
AppBar: AppBarAction[];
UserGuideDropdown: UserGuideDropdownAction[];
FileUploadMethod: FileUploadMethodAction[];
ChannelIntroButton: ChannelIntroButtonAction[];
FilesDropdown: FilesDropdownAction[];
Product: ProductComponent[];
CallButton: PluginComponent[];
PostDropdownMenu: PluginComponent[];
PostAction: PluginComponent[];
PostEditorAction: PluginComponent[];
CodeBlockAction: PluginComponent[];
NewMessagesSeparatorAction: PluginComponent[];
FilePreview: PluginComponent[];
MainMenu: PluginComponent[];
LinkTooltip: PluginComponent[];
RightHandSidebarComponent: PluginComponent[];
ChannelHeaderButton: PluginComponent[];
MobileChannelHeaderButton: PluginComponent[];
AppBar: AppBarComponent[];
UserGuideDropdownItem: PluginComponent[];
FilesWillUploadHook: PluginComponent[];
PostDropdownMenuItem: PostDropdownMenuItemComponent[];
PostAction: PostActionComponent[];
PostEditorAction: PostEditorActionComponent[];
CodeBlockAction: CodeBlockActionComponent[];
NewMessagesSeparatorAction: NewMessagesSeparatorActionComponent[];
FilePreview: FilePreviewComponent[];
LinkTooltip: LinkTooltipComponent[];
RightHandSidebarComponent: RightHandSidebarComponent[];
NeedsTeamComponent: NeedsTeamComponent[];
CreateBoardFromTemplate: PluginComponent[];
CreateBoardFromTemplate: CreateBoardFromTemplateComponent[];
SearchHints: SearchHintsComponent[];
SearchSuggestions: SearchSuggestionsComponent[];
SearchButtons: SearchButtonsComponent[];
PostWillRenderEmbedComponent: PostWillRenderEmbedComponent[];
PopoverUserAttributes: PopoverUserAttributesComponent[];
PopoverUserActions: PopoverUserActionsComponent[];
LeftSidebarHeader: LeftSidebarHeaderComponent[];
Root: RootComponent[];
BottomTeamSidebar: BottomTeamSidebarComponent[];
PostMessageAttachment: PostMessageAttachmentComponent[];
CustomRouteComponent: CustomRouteComponent[];
Global: GlobalComponent[];
ChannelToast: ChannelToastComponent[];
SidebarChannelLinkLabel: SidebarChannelLinkLabelComponent[];
FilesWillUploadHook: FilesWillUploadHook[];
DesktopNotificationHooks: DesktopNotificationHook[];
SlashCommandWillBePosted: SlashCommandWillBePostedHook[];
MessageWillBePosted: MessageWillBePostedHook[];
MessageWillBeUpdated: MessageWillBeUpdatedHook[];
MessageWillFormat: MessageWillFormatHook[];
MessageWillBePosted: MessageWillBePostedHook[];
SlashCommandWillBePosted: SlashCommandWillBePostedHook[];
MessageWillBeUpdated: MessageWillBeUpdatedHook[];
};
postTypes: {
@@ -86,106 +110,128 @@ export type PluginsState = {
export type Menu = {
id: string;
parentMenuId?: string;
text?: React.ReactElement | string;
text?: PluggableText;
selectedValueText?: string;
subMenu?: Menu[];
filter?: (id?: string) => boolean;
filter?: (id: string) => boolean;
action?: (...args: any) => void;
icon?: React.ReactElement;
icon?: React.ReactNode;
direction?: 'left' | 'right';
isHeader?: boolean;
}
export type PluginComponent = {
type PluginComponent = {
id: string;
pluginId: string;
title?: string;
/** @default null - which means 'channels'*/
supportedProductIds?: ProductScope;
component?: React.ComponentType;
subMenu?: Menu[];
text?: string;
dropdownText?: string;
tooltipText?: string;
button?: React.ReactElement;
dropdownButton?: React.ReactElement;
icon?: React.ReactElement;
iconUrl?: string;
mobileIcon?: React.ReactElement;
filter?: (id: string) => boolean;
action?: (...args: any) => void; // TODO Add more concrete types?
shouldRender?: (state: GlobalState) => boolean;
};
export type AppBarComponent = PluginComponent & {
rhsComponentId?: string;
type BasePluggableProps = {
webSocketClient: WebSocketClient;
theme: Theme;
}
export type NeedsTeamComponent = PluginComponent & {
route: string;
}
export type PluggableText = string | React.ReactNode;
export type FilesWillUploadHook = {
hook: (files: File[], uploadFiles: (files: File[]) => void) => { message?: string; files?: File[] };
}
export type AppBarChannelAction = (channel: Channel, member: ChannelMembership) => void;
export type AppBarAction = PluginComponent & {
iconUrl: string;
supportedProductIds: ProductScope;
tooltipText: PluggableText;
} & ({
action: AppBarChannelAction;
} | {
rhsComponentId: string;
action: () => {data: boolean};
});
export type FilePreviewComponent = {
id: string;
pluginId: string;
override: (fileInfo: FileInfo, post?: Post) => boolean;
component: React.ComponentType<{ fileInfo: FileInfo; post?: Post; onModalDismissed: () => void }>;
}
export type FileDropdownPluginComponent = {
id: string;
pluginId: string;
text: string | React.ReactElement;
export type FilesDropdownAction = PluginComponent & {
text: PluggableText;
match: (fileInfo: FileInfo) => boolean;
action: (fileInfo: FileInfo) => void;
};
export type PostPluginComponent = {
id: string;
pluginId: string;
type: string;
component: React.ElementType;
export type PostDropdownMenuAction = PluginComponent & {
parentMenuId?: string;
subMenu?: PostDropdownMenuAction[];
text: PluggableText;
action: (postId: string) => void;
filter: (postId: string) => boolean;
};
export type AdminConsolePluginComponent = {
pluginId: string;
key: string;
component: React.Component;
options: {
showTitle: boolean;
};
export type ChannelHeaderAction = PluginComponent & {
text: PluggableText;
action: (channelId: string) => void;
shouldRender: (state: GlobalState) => boolean;
};
export type AdminConsolePluginCustomSection = {
pluginId: string;
key: string;
component: React.Component;
export type ChannelHeaderButtonAction = PluginComponent & {
icon: React.ReactNode;
dropdownText: PluggableText;
tooltipText: PluggableText;
action: (channel: Channel, member?: ChannelMembership) => void;
};
export type PostWillRenderEmbedPluginComponent = {
id: string;
pluginId: string;
component: React.ComponentType<{ embed: PostEmbed; webSocketClient?: WebSocketClient }>;
match: (arg: PostEmbed) => boolean;
toggleable: boolean;
export type FileUploadMethodAction = PluginComponent & {
text: PluggableText;
action: (checkPluginHooksAndUploadFiles: ((files: FileList | File[]) => void)) => void;
icon: React.ReactNode;
};
export type MainMenuAction = PluginComponent & {
text: PluggableText;
action: () => void;
mobileIcon: React.ReactNode;
};
export type ChannelIntroButtonAction = PluginComponent & {
text: PluggableText;
action: (channel: Channel, member: ChannelMembership) => void;
icon: React.ReactNode;
};
export type UserGuideDropdownAction = PluginComponent & {
text: PluggableText;
action: (fileInfo: FileInfo) => void;
};
export type CallButtonAction = PluginComponent & {
button: React.ReactNode;
dropdownButton: React.ReactNode;
action: (channel?: Channel | null, member?: ChannelMembership) => void;
};
export type MobileChannelHeaderButtonAction = PluginComponent & {
button?: CallButtonAction['button'];
dropdownButton?: CallButtonAction['dropdownButton'];
icon: ChannelHeaderButtonAction['icon'];
action: ChannelHeaderButtonAction['action'];
dropdownText?: ChannelHeaderButtonAction['dropdownText'];
tooltipText?: ChannelHeaderButtonAction['tooltipText'];
};
export type DesktopNotificationArgs = {
title: string;
body: string;
silent: boolean;
soundName: string;
url: string;
notify: boolean;
};
export type DesktopNotificationHook = PluginComponent & {
hook: (post: Post, msgProps: NewPostMessageProps, channel: Channel, teamId: string, args: DesktopNotificationArgs) => Promise<{
error?: string;
args?: DesktopNotificationArgs;
}>;
}
export type ProductComponent = {
export type FilesWillUploadHook = PluginComponent & {
hook: (files: File[], uploadFiles: (files: File[]) => void) => { message?: string; files?: File[] };
}
/**
* The main uuid of the product.
*/
id: string;
/**
* The plain identifier of the source plugin
*/
pluginId: string;
type ProductBaseProps = {theme: Theme};
export type ProductSubComponentNames = 'mainComponent' | 'publicComponent' | 'headerCentreComponent' | 'headerRightComponent';
export type ProductComponent = PluginComponent & {
/**
* A compass-icon glyph to display as the icon in the product switcher
@@ -210,24 +256,26 @@ export type ProductComponent = {
/**
* The component to be displayed below the global header when your route is active.
*/
mainComponent: React.ComponentType;
mainComponent: React.ComponentType<ProductBaseProps & {
webSocketClient: WebSocketClient;
}>;
/**
* The public component to be displayed when a public route is active.
*/
publicComponent: React.ComponentType | null;
publicComponent: React.ComponentType<ProductBaseProps & RouteComponentProps>;
/**
* A component to fill the generic area in the center of
* the global header when your route is active.
*/
headerCentreComponent: React.ComponentType;
headerCentreComponent: React.ComponentType<ProductBaseProps>;
/**
* A component to fill the generic area in the right of
* the global header when your route is active.
*/
headerRightComponent: React.ComponentType;
headerRightComponent: React.ComponentType<ProductBaseProps>;
/**
* A flag to display or hide the team sidebar in products.
@@ -248,45 +296,191 @@ export type ProductComponent = {
wrapped: boolean;
};
export type DesktopNotificationArgs = {
title: string;
body: string;
silent: boolean;
soundName: string;
url: string;
notify: boolean;
};
export type NeedsTeamComponent = PluginComponent & {
route: string;
component: React.ComponentType<BasePluggableProps>;
}
export type DesktopNotificationHook = PluginComponent & {
hook: (post: Post, msgProps: NewPostMessageProps, channel: Channel, teamId: string, args: DesktopNotificationArgs) => Promise<{
error?: string;
args?: DesktopNotificationArgs;
export type FilePreviewComponent = PluginComponent & {
override: (fileInfo: FileInfo, post?: Post) => boolean;
component: React.ComponentType<{
fileInfo: FileInfo;
post?: Post;
onModalDismissed: () => void;
}>;
}
type SlashCommandWillBePostedArgs = {
channel_id: string;
team_id?: string;
root_id?: string;
export type PostWillRenderEmbedComponent = PluginComponent & {
component: React.ComponentType<{
embed: PostEmbed;
webSocketClient?: WebSocketClient;
}>;
match: (arg: PostEmbed) => boolean;
toggleable: boolean;
}
export type SlashCommandWillBePostedHook = PluginComponent & {
hook: (message: string, args: SlashCommandWillBePostedArgs) => Promise<(
{error: {message: string}} | {message: string; args: SlashCommandWillBePostedArgs} | Record<string, never>
)>;
export type PostDropdownMenuItemComponent = PluginComponent & {
text: PluggableText;
component: React.ComponentType<BasePluggableProps & {postId: string}>;
};
export type MessageWillBePostedHook = PluginComponent & {
hook: (post: Post) => Promise<(
{error: {message: string}} | {post: Post}
)>;
export type RightHandSidebarComponent = PluginComponent & {
title: PluggableText;
component: React.ComponentType<BasePluggableProps>;
};
export type MessageWillBeUpdatedHook = PluginComponent & {
hook: (newPost: Partial<Post>, oldPost: Post) => Promise<(
{error: {message: string}} | {post: Partial<Post>}
)>;
export type SearchHintsComponent = PluginComponent & {
component: React.ComponentType<{
onChangeSearch: (value: string, matchedPretext: string) => void;
searchTerms: string;
}>;
};
export type SearchSuggestionsComponent = PluginComponent & {
component: React.ComponentType<{
searchTerms: string;
onChangeSearch: (value: string, matchedPretext: string) => void;
onRunSearch: (searchTerms: string) => void;
}>;
};
export type SearchButtonsComponent = PluginComponent & {
component: React.ComponentType; // Review the props
action: (terms: string) => void;
};
export type PostActionComponent = PluginComponent & {
component: React.ComponentType<{
post: Post;
}>;
};
export type NewMessagesSeparatorActionComponent = PluginComponent & {
component: React.ComponentType<{
lastViewedAt: number;
channelId?: string;
threadId?: string;
}>;
};
export type PopoverUserAttributesComponent = PluginComponent & {
component: React.ComponentType<BasePluggableProps & {
user: UserProfile;
hide?: () => void;
status: string | null;
fromWebhook?: boolean;
}>;
};
export type PopoverUserActionsComponent = PluginComponent & {
component: React.ComponentType<BasePluggableProps & {
user: UserProfile;
hide?: () => void;
status: string | null;
}>;
};
export type LeftSidebarHeaderComponent = PluginComponent & {
component: React.ComponentType<BasePluggableProps>;
};
export type RootComponent = PluginComponent & {
component: React.ComponentType<BasePluggableProps>;
};
export type BottomTeamSidebarComponent = PluginComponent & {
component: React.ComponentType<BasePluggableProps>;
};
export type SidebarChannelLinkLabelComponent = PluginComponent & {
component: React.ComponentType<BasePluggableProps & {
channel: Channel;
}>;
};
export type PostMessageAttachmentComponent = PluginComponent & {
component: React.ComponentType<BasePluggableProps & {
postId: string;
onHeightChange: (height: number) => void;
}>;
};
export type LinkTooltipComponent = PluginComponent & {
component: React.ComponentType<BasePluggableProps & {
href: string;
show: boolean;
}>;
};
export type PostEditorActionComponent = PluginComponent & {
component: React.ComponentType;
};
export type CodeBlockActionComponent = PluginComponent & {
component: React.ComponentType;
};
export type CustomRouteComponent = PluginComponent & {
component: React.ComponentType;
route: string;
};
export type GlobalComponent = PluginComponent & {
component: React.ComponentType;
};
export type ChannelToastComponent = PluginComponent & {
component: React.ComponentType<BasePluggableProps>;
}
export type CreateBoardFromTemplateComponent = PluginComponent & {
component: React.ComponentType<BasePluggableProps & {
setCanCreate: (v: boolean) => void;
setAction: (action: ((currentTeamId: string, channelId: string) => Promise<Board>) | undefined) => void;
newBoardInfoIcon: React.JSX.Element;
}>;
action: () => void;
};
export type MessageWillFormatHook = PluginComponent & {
hook: (post: Post, message: string) => string;
};
export type MessageWillBePostedHook = PluginComponent & {
hook: (post: Post) => Promise<{error: {message: string}} | {post: Post}>;
};
export type SlashCommandWillBePostedHook = PluginComponent & {
hook: (message: string, args: CommandArgs) => Promise<{error: {message: string}} | {message: string; args: CommandArgs} | Record<string, never>>;
};
export type MessageWillBeUpdatedHook = PluginComponent & {
hook: (post: Partial<Post>, oldPost: Post) => Promise<{error: {message: string}} | {post: Post}>;
};
export type PostPluginComponent = {
id: string;
pluginId: string;
type: string;
component: React.ComponentType<{
post: Post;
compactDisplay?: boolean;
isRHS?: boolean;
theme?: Theme;
}>;
};
export type AdminConsolePluginComponent = {
pluginId: string;
key: string;
component: React.Component;
options: {
showTitle: boolean;
};
};
export type AdminConsolePluginCustomSection = {
pluginId: string;
key: string;
component: React.Component;
};

Просмотреть файл

@@ -432,7 +432,7 @@ export class TestHelper {
showTeamSidebar: false,
showAppBar: false,
wrapped: true,
publicComponent: null,
publicComponent: () => null,
};
}