Fix pluggables types (#28986)
* Fix pluggables types * Address feedback * Fix lint
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
cb75a20c54
Коммит
4a59d4c08e
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user