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"
|
key="the_component_id_pluginmenuitem"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
show={true}
|
show={true}
|
||||||
|
text="Some text"
|
||||||
/>
|
/>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
key="post_id_1pluggable"
|
key="post_id_1pluggable"
|
||||||
pluggableName="PostDropdownMenuItem"
|
pluggableName="PostDropdownMenuItem"
|
||||||
postId="post_id_1"
|
postId="post_id_1"
|
||||||
@@ -88,8 +89,9 @@ exports[`components/actions_menu/ActionsMenu has actions - marketplace enabled a
|
|||||||
key="the_component_id_pluginmenuitem"
|
key="the_component_id_pluginmenuitem"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
show={true}
|
show={true}
|
||||||
|
text="Some text"
|
||||||
/>
|
/>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
key="post_id_1pluggable"
|
key="post_id_1pluggable"
|
||||||
pluggableName="PostDropdownMenuItem"
|
pluggableName="PostDropdownMenuItem"
|
||||||
postId="post_id_1"
|
postId="post_id_1"
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import type {PostType} from '@mattermost/types/posts';
|
|||||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||||
import {TestHelper} from 'utils/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';
|
import type {Props} from './actions_menu';
|
||||||
|
|
||||||
jest.mock('utils/utils', () => {
|
jest.mock('utils/utils', () => {
|
||||||
@@ -21,11 +21,13 @@ jest.mock('utils/utils', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropdownComponents: PluginComponent[] = [
|
const dropdownComponents: PostDropdownMenuAction[] = [
|
||||||
{
|
{
|
||||||
id: 'the_component_id',
|
id: 'the_component_id',
|
||||||
pluginId: 'playbooks',
|
pluginId: 'playbooks',
|
||||||
|
text: 'Some text',
|
||||||
action: jest.fn(),
|
action: jest.fn(),
|
||||||
|
filter: () => true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ describe('components/actions_menu/ActionsMenu', () => {
|
|||||||
isSysAdmin: true,
|
isSysAdmin: true,
|
||||||
pluginMenuItems: [],
|
pluginMenuItems: [],
|
||||||
post: TestHelper.getPostMock({id: 'post_id_1', is_pinned: false, type: '' as PostType}),
|
post: TestHelper.getPostMock({id: 'post_id_1', is_pinned: false, type: '' as PostType}),
|
||||||
components: {},
|
pluginMenuItemComponents: [],
|
||||||
location: 'center',
|
location: 'center',
|
||||||
canOpenMarketplace: false,
|
canOpenMarketplace: false,
|
||||||
actions: {
|
actions: {
|
||||||
@@ -117,9 +119,7 @@ describe('components/actions_menu/ActionsMenu', () => {
|
|||||||
expect(wrapper.find('#divider_post_post_id_1_marketplace').exists()).toBe(false);
|
expect(wrapper.find('#divider_post_post_id_1_marketplace').exists()).toBe(false);
|
||||||
|
|
||||||
wrapper.setProps({
|
wrapper.setProps({
|
||||||
components: {
|
pluginMenuItemComponents: dropdownComponents,
|
||||||
[PLUGGABLE_COMPONENT]: dropdownComponents,
|
|
||||||
},
|
|
||||||
canOpenMarketplace: true,
|
canOpenMarketplace: true,
|
||||||
});
|
});
|
||||||
expect(wrapper.find('#divider_post_post_id_1_marketplace').exists()).toBe(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);
|
expect(wrapper.find('#divider_post_post_id_1_marketplace').exists()).toBe(false);
|
||||||
|
|
||||||
wrapper.setProps({
|
wrapper.setProps({
|
||||||
components: {
|
pluginMenuItemComponents: dropdownComponents,
|
||||||
[PLUGGABLE_COMPONENT]: dropdownComponents,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(wrapper.find('#divider_post_post_id_1_marketplace').exists()).toBe(false);
|
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 {ModalData} from 'types/actions';
|
||||||
import type {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForPost} from 'types/apps';
|
import type {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForPost} from 'types/apps';
|
||||||
import type {PluginComponent} from 'types/store/plugins';
|
import type {PostDropdownMenuAction, PostDropdownMenuItemComponent} from 'types/store/plugins';
|
||||||
|
|
||||||
import './actions_menu.scss';
|
|
||||||
|
|
||||||
import {ActionsMenuIcon} from './actions_menu_icon';
|
import {ActionsMenuIcon} from './actions_menu_icon';
|
||||||
|
|
||||||
|
import './actions_menu.scss';
|
||||||
|
|
||||||
const MENU_BOTTOM_MARGIN = 80;
|
const MENU_BOTTOM_MARGIN = 80;
|
||||||
|
|
||||||
export const PLUGGABLE_COMPONENT = 'PostDropdownMenuItem';
|
export const PLUGGABLE_COMPONENT = 'PostDropdownMenuItem';
|
||||||
@@ -44,7 +44,7 @@ export type Props = {
|
|||||||
isMenuOpen?: boolean;
|
isMenuOpen?: boolean;
|
||||||
isSysAdmin: boolean;
|
isSysAdmin: boolean;
|
||||||
location?: 'CENTER' | 'RHS_ROOT' | 'RHS_COMMENT' | 'SEARCH' | string;
|
location?: 'CENTER' | 'RHS_ROOT' | 'RHS_COMMENT' | 'SEARCH' | string;
|
||||||
pluginMenuItems?: PluginComponent[];
|
pluginMenuItems?: PostDropdownMenuAction[];
|
||||||
post: Post;
|
post: Post;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
canOpenMarketplace: boolean;
|
canOpenMarketplace: boolean;
|
||||||
@@ -52,9 +52,7 @@ export type Props = {
|
|||||||
/**
|
/**
|
||||||
* Components for overriding provided by plugins
|
* Components for overriding provided by plugins
|
||||||
*/
|
*/
|
||||||
components: {
|
pluginMenuItemComponents: PostDropdownMenuItemComponent[];
|
||||||
[componentName: string]: PluginComponent[];
|
|
||||||
};
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
@@ -346,7 +344,7 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
let menuItems;
|
let menuItems;
|
||||||
const hasApps = Boolean(appBindings.length);
|
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 hasPluginItems = Boolean(pluginItems?.length);
|
||||||
|
|
||||||
const hasPluginMenuItems = hasPluginItems || hasApps || hasPluggables;
|
const hasPluginMenuItems = hasPluginItems || hasApps || hasPluggables;
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ describe('components/actions_menu/ActionsMenu returning empty ("")', () => {
|
|||||||
test('should match snapshot, return empty ("") on Center', () => {
|
test('should match snapshot, return empty ("") on Center', () => {
|
||||||
const baseProps: Omit<Props, 'intl'> = {
|
const baseProps: Omit<Props, 'intl'> = {
|
||||||
post: TestHelper.getPostMock({id: 'post_id_1'}),
|
post: TestHelper.getPostMock({id: 'post_id_1'}),
|
||||||
components: {},
|
|
||||||
teamId: 'team_id_1',
|
teamId: 'team_id_1',
|
||||||
actions: {
|
actions: {
|
||||||
openModal: jest.fn(),
|
openModal: jest.fn(),
|
||||||
@@ -42,6 +41,7 @@ describe('components/actions_menu/ActionsMenu returning empty ("")', () => {
|
|||||||
appsEnabled: false,
|
appsEnabled: false,
|
||||||
isSysAdmin: true,
|
isSysAdmin: true,
|
||||||
canOpenMarketplace: false,
|
canOpenMarketplace: false,
|
||||||
|
pluginMenuItemComponents: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ describe('components/actions_menu/ActionsMenu on mobile view', () => {
|
|||||||
test('should match snapshot', () => {
|
test('should match snapshot', () => {
|
||||||
const baseProps: Omit<Props, 'intl'> = {
|
const baseProps: Omit<Props, 'intl'> = {
|
||||||
post: TestHelper.getPostMock({id: 'post_id_1'}),
|
post: TestHelper.getPostMock({id: 'post_id_1'}),
|
||||||
components: {},
|
|
||||||
teamId: 'team_id_1',
|
teamId: 'team_id_1',
|
||||||
actions: {
|
actions: {
|
||||||
openModal: jest.fn(),
|
openModal: jest.fn(),
|
||||||
@@ -42,6 +41,7 @@ describe('components/actions_menu/ActionsMenu on mobile view', () => {
|
|||||||
appsEnabled: false,
|
appsEnabled: false,
|
||||||
isSysAdmin: true,
|
isSysAdmin: true,
|
||||||
canOpenMarketplace: false,
|
canOpenMarketplace: false,
|
||||||
|
pluginMenuItemComponents: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
|
|||||||
return {
|
return {
|
||||||
appBindings,
|
appBindings,
|
||||||
appsEnabled: apps,
|
appsEnabled: apps,
|
||||||
components: state.plugins.components,
|
pluginMenuItemComponents: state.plugins.components.PostDropdownMenuItem,
|
||||||
isSysAdmin,
|
isSysAdmin,
|
||||||
pluginMenuItems: state.plugins.components.PostDropdownMenu,
|
pluginMenuItems: state.plugins.components.PostDropdownMenu,
|
||||||
teamId: getCurrentTeamId(state),
|
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 {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||||
import {TestHelper} from 'utils/test_helper';
|
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';
|
import AppBar from './app_bar';
|
||||||
|
|
||||||
describe('components/app_bar/app_bar', () => {
|
describe('components/app_bar/app_bar', () => {
|
||||||
const channelHeaderComponents: PluginComponent[] = [
|
const channelHeaderComponents: ChannelHeaderButtonAction[] = [
|
||||||
{
|
{
|
||||||
id: 'the_component_id',
|
id: 'the_component_id',
|
||||||
pluginId: 'playbooks',
|
pluginId: 'playbooks',
|
||||||
icon: 'fallback_component' as any,
|
icon: 'fallback_component' as any,
|
||||||
tooltipText: 'Playbooks Tooltip',
|
tooltipText: 'Playbooks Tooltip',
|
||||||
action: jest.fn(),
|
action: jest.fn(),
|
||||||
|
dropdownText: 'Playbooks dropdown',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rhsComponents: PluginComponent[] = [
|
const rhsComponents: RightHandSidebarComponent[] = [
|
||||||
{
|
{
|
||||||
id: 'the_rhs_plugin_component_id',
|
id: 'the_rhs_plugin_component_id',
|
||||||
pluginId: 'playbooks',
|
pluginId: 'playbooks',
|
||||||
icon: <div/>,
|
component: () => null,
|
||||||
action: jest.fn(),
|
title: 'some title',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ describe('components/app_bar/app_bar', () => {
|
|||||||
AppBar: channelHeaderComponents,
|
AppBar: channelHeaderComponents,
|
||||||
RightHandSidebarComponent: rhsComponents,
|
RightHandSidebarComponent: rhsComponents,
|
||||||
Product: [],
|
Product: [],
|
||||||
} as {[componentName: string]: PluginComponent[]},
|
},
|
||||||
},
|
},
|
||||||
entities: {
|
entities: {
|
||||||
apps: {
|
apps: {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import partition from 'lodash/partition';
|
import partition from 'lodash/partition';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type {ReactNode} from 'react';
|
|
||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
import type {GlobalState} from '@mattermost/types/store';
|
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 AppBarBinding, {isAppBinding} from './app_bar_binding';
|
||||||
import AppBarMarketplace from './app_bar_marketplace';
|
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';
|
import './app_bar.scss';
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ export default function AppBar() {
|
|||||||
return coreProductsPluginIds.includes(pluginId);
|
return coreProductsPluginIds.includes(pluginId);
|
||||||
});
|
});
|
||||||
|
|
||||||
const items: ReactNode[] = [
|
const items = [
|
||||||
...coreProductComponents,
|
...coreProductComponents,
|
||||||
getDivider(coreProductComponents.length, (pluginComponents.length + channelHeaderComponents.length + appBarBindings.length)),
|
getDivider(coreProductComponents.length, (pluginComponents.length + channelHeaderComponents.length + appBarBindings.length)),
|
||||||
...pluginComponents,
|
...pluginComponents,
|
||||||
@@ -60,8 +59,9 @@ export default function AppBar() {
|
|||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAppBarPluginComponent(x)) {
|
if (isAppBarComponent(x)) {
|
||||||
if (!inScope(x.supportedProductIds ?? null, currentProductId, currentProduct?.pluginId)) {
|
const supportedProductIds = 'supportedProductIds' in x ? x.supportedProductIds : undefined;
|
||||||
|
if (!inScope(supportedProductIds ?? null, currentProductId, currentProduct?.pluginId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import WithTooltip from 'components/with_tooltip';
|
|||||||
|
|
||||||
import {suitePluginIds} from 'utils/constants';
|
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';
|
import NewChannelWithBoardTourTip from './new_channel_with_board_tour_tip';
|
||||||
|
|
||||||
type PluginComponentProps = {
|
type AppBarComponentProps = {
|
||||||
component: AppBarComponent;
|
component: ChannelHeaderButtonAction | AppBarAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ImageLoadState {
|
enum ImageLoadState {
|
||||||
@@ -28,22 +28,27 @@ enum ImageLoadState {
|
|||||||
ERROR = 'error',
|
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);
|
return Boolean(x?.id && x?.pluginId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppBarPluginComponent = (props: PluginComponentProps) => {
|
const AppBarPluginComponent = ({
|
||||||
const {component} = props;
|
component,
|
||||||
|
}: AppBarComponentProps) => {
|
||||||
const channel = useSelector(getCurrentChannel);
|
const channel = useSelector(getCurrentChannel);
|
||||||
const channelMember = useSelector(getMyCurrentChannelMembership);
|
const channelMember = useSelector(getMyCurrentChannelMembership);
|
||||||
const activeRhsComponent = useSelector(getActiveRhsComponent);
|
const activeRhsComponent = useSelector(getActiveRhsComponent);
|
||||||
|
|
||||||
const [imageLoadState, setImageLoadState] = useState<ImageLoadState>(ImageLoadState.LOADING);
|
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(() => {
|
useEffect(() => {
|
||||||
setImageLoadState(ImageLoadState.LOADING);
|
setImageLoadState(ImageLoadState.LOADING);
|
||||||
}, [component.iconUrl]);
|
}, [iconUrl]);
|
||||||
|
|
||||||
const onImageLoadComplete = () => {
|
const onImageLoadComplete = () => {
|
||||||
setImageLoadState(ImageLoadState.LOADED);
|
setImageLoadState(ImageLoadState.LOADED);
|
||||||
@@ -54,9 +59,8 @@ const AppBarPluginComponent = (props: PluginComponentProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buttonId = `app-bar-icon-${component.pluginId}`;
|
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 = (
|
let content: React.ReactNode = (
|
||||||
<div
|
<div
|
||||||
role='button'
|
role='button'
|
||||||
@@ -72,7 +76,7 @@ const AppBarPluginComponent = (props: PluginComponentProps) => {
|
|||||||
</div>
|
</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) {
|
if (!iconUrl) {
|
||||||
content = (
|
content = (
|
||||||
@@ -81,7 +85,7 @@ const AppBarPluginComponent = (props: PluginComponentProps) => {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={classNames('app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered', {'app-bar__old-icon--active': isButtonActive})}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -103,7 +107,13 @@ const AppBarPluginComponent = (props: PluginComponentProps) => {
|
|||||||
id={buttonId}
|
id={buttonId}
|
||||||
className={classNames('app-bar__icon', {'app-bar__icon--active': isButtonActive})}
|
className={classNames('app-bar__icon', {'app-bar__icon--active': isButtonActive})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (channel && channelMember) {
|
||||||
component.action?.(channel, channelMember);
|
component.action?.(channel, channelMember);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ('rhsComponentId' in component) {
|
||||||
|
component.action();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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) {
|
export function makeAsyncComponent<ComponentProps>(displayName: string, LazyComponent: React.ComponentType<ComponentProps>, fallback: React.ReactNode = null) {
|
||||||
const Component: ComponentType<ComponentProps> = (props) => (
|
const Component: ComponentType<ComponentProps> = (props) => (
|
||||||
@@ -12,3 +16,17 @@ export function makeAsyncComponent<ComponentProps>(displayName: string, LazyComp
|
|||||||
Component.displayName = displayName;
|
Component.displayName = displayName;
|
||||||
return Component;
|
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 = {
|
const props: Props = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
pluginMenuItems: [
|
pluginMenuItems: [
|
||||||
{id: 'plugin-1', pluginId: 'playbooks', action: jest.fn(), text: 'plugin-1-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'},
|
{id: 'plugin-2', pluginId: 'playbooks', action: jest.fn(), text: 'plugin-2-text', shouldRender: () => true},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<ChannelHeaderDropdown {...props}/>);
|
const wrapper = shallow(<ChannelHeaderDropdown {...props}/>);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import MobileChannelHeaderPlug from 'plugins/mobile_channel_header_plug';
|
|||||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||||
import {localizeMessage} from 'utils/utils';
|
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 MenuItemCloseChannel from './menu_items/close_channel';
|
||||||
import MenuItemCloseMessage from './menu_items/close_message';
|
import MenuItemCloseMessage from './menu_items/close_message';
|
||||||
@@ -51,7 +51,7 @@ export type Props = {
|
|||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
penultimateViewedChannelName: string;
|
penultimateViewedChannelName: string;
|
||||||
pluginMenuItems: PluginComponent[];
|
pluginMenuItems: ChannelHeaderAction[];
|
||||||
isLicensedForLDAPGroups: boolean;
|
isLicensedForLDAPGroups: boolean;
|
||||||
isChannelBookmarksEnabled: boolean;
|
isChannelBookmarksEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {makeAsyncComponent} from 'components/async_load';
|
|||||||
import {canDownloadFiles} from 'utils/file_utils';
|
import {canDownloadFiles} from 'utils/file_utils';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
import type {FilePreviewComponent} from 'types/store/plugins';
|
|
||||||
|
|
||||||
import type {Props} from './file_preview_modal';
|
import type {Props} from './file_preview_modal';
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||||||
canDownloadFiles: canDownloadFiles(config),
|
canDownloadFiles: canDownloadFiles(config),
|
||||||
enablePublicLink: config.EnablePublicLink === 'true',
|
enablePublicLink: config.EnablePublicLink === 'true',
|
||||||
isMobileView: getIsMobileView(state),
|
isMobileView: getIsMobileView(state),
|
||||||
pluginFilePreviewComponents: state.plugins.components.FilePreview as unknown as FilePreviewComponent[],
|
pluginFilePreviewComponents: state.plugins.components.FilePreview,
|
||||||
post: ownProps.post || getPost(state, ownProps.postId || ''),
|
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 {openModal} from 'actions/views/modals';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
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';
|
import FileSearchResultItem from './file_search_result_item';
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export type OwnProps = {
|
|||||||
channelId: string;
|
channelId: string;
|
||||||
fileInfo: FileInfo;
|
fileInfo: FileInfo;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
pluginMenuItems?: FileDropdownPluginComponent[];
|
pluginMenuItems?: FilesDropdownAction[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
isTextDroppableEvent,
|
isTextDroppableEvent,
|
||||||
} from 'utils/utils';
|
} from 'utils/utils';
|
||||||
|
|
||||||
import type {FilesWillUploadHook, PluginComponent} from 'types/store/plugins';
|
import type {FilesWillUploadHook, FileUploadMethodAction} from 'types/store/plugins';
|
||||||
|
|
||||||
const holders = defineMessages({
|
const holders = defineMessages({
|
||||||
limited: {
|
limited: {
|
||||||
@@ -147,7 +147,7 @@ export type Props = {
|
|||||||
/**
|
/**
|
||||||
* Plugin file upload methods to be added
|
* Plugin file upload methods to be added
|
||||||
*/
|
*/
|
||||||
pluginFileUploadMethods: PluginComponent[];
|
pluginFileUploadMethods: FileUploadMethodAction[];
|
||||||
pluginFilesWillUploadHooks: FilesWillUploadHook[];
|
pluginFilesWillUploadHooks: FilesWillUploadHook[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {getEditingPostDetailsAndPost} from 'selectors/posts';
|
|||||||
import {canUploadFiles} from 'utils/file_utils';
|
import {canUploadFiles} from 'utils/file_utils';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
import type {FilesWillUploadHook} from 'types/store/plugins';
|
|
||||||
|
|
||||||
import FileUpload from './file_upload';
|
import FileUpload from './file_upload';
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
canUploadFiles: canUploadFiles(config),
|
canUploadFiles: canUploadFiles(config),
|
||||||
locale: getCurrentLocale(state),
|
locale: getCurrentLocale(state),
|
||||||
pluginFileUploadMethods: state.plugins.components.FileUploadMethod,
|
pluginFileUploadMethods: state.plugins.components.FileUploadMethod,
|
||||||
pluginFilesWillUploadHooks: state.plugins.components.FilesWillUploadHook as unknown as FilesWillUploadHook[],
|
pluginFilesWillUploadHooks: state.plugins.components.FilesWillUploadHook,
|
||||||
centerChannelPostBeingEdited,
|
centerChannelPostBeingEdited,
|
||||||
rhsPostBeingEdited,
|
rhsPostBeingEdited,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1899,6 +1899,7 @@ exports[`components/Menu should match snapshot with plugins 1`] = `
|
|||||||
key="plugin-id-1_pluginmenuitem"
|
key="plugin-id-1_pluginmenuitem"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
show={true}
|
show={true}
|
||||||
|
text="some text"
|
||||||
/>
|
/>
|
||||||
<MenuItemAction
|
<MenuItemAction
|
||||||
icon={false}
|
icon={false}
|
||||||
@@ -1906,6 +1907,7 @@ exports[`components/Menu should match snapshot with plugins 1`] = `
|
|||||||
key="plugind-id-2_pluginmenuitem"
|
key="plugind-id-2_pluginmenuitem"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
show={true}
|
show={true}
|
||||||
|
text="some text"
|
||||||
/>
|
/>
|
||||||
</Memo(MenuGroup)>
|
</Memo(MenuGroup)>
|
||||||
</Menu>
|
</Menu>
|
||||||
@@ -2249,16 +2251,28 @@ exports[`components/Menu should match snapshot with plugins in mobile 1`] = `
|
|||||||
</Memo(MenuGroup)>
|
</Memo(MenuGroup)>
|
||||||
<Memo(MenuGroup)>
|
<Memo(MenuGroup)>
|
||||||
<MenuItemAction
|
<MenuItemAction
|
||||||
|
icon={
|
||||||
|
<i
|
||||||
|
className="fa fa-anchor"
|
||||||
|
/>
|
||||||
|
}
|
||||||
id="plugin-id-1_pluginmenuitem"
|
id="plugin-id-1_pluginmenuitem"
|
||||||
key="plugin-id-1_pluginmenuitem"
|
key="plugin-id-1_pluginmenuitem"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
show={true}
|
show={true}
|
||||||
|
text="some text"
|
||||||
/>
|
/>
|
||||||
<MenuItemAction
|
<MenuItemAction
|
||||||
|
icon={
|
||||||
|
<i
|
||||||
|
className="fa fa-anchor"
|
||||||
|
/>
|
||||||
|
}
|
||||||
id="plugind-id-2_pluginmenuitem"
|
id="plugind-id-2_pluginmenuitem"
|
||||||
key="plugind-id-2_pluginmenuitem"
|
key="plugind-id-2_pluginmenuitem"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
show={true}
|
show={true}
|
||||||
|
text="some text"
|
||||||
/>
|
/>
|
||||||
</Memo(MenuGroup)>
|
</Memo(MenuGroup)>
|
||||||
<Memo(MenuGroup)>
|
<Memo(MenuGroup)>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
|
import type {ComponentProps} from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {createIntl} from 'react-intl';
|
import {createIntl} from 'react-intl';
|
||||||
import {Provider} from 'react-redux';
|
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 {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||||
import mockStore from 'tests/test_store';
|
import mockStore from 'tests/test_store';
|
||||||
import {Constants} from 'utils/constants';
|
|
||||||
import {TestHelper} from 'utils/test_helper';
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
import {MainMenu} from './main_menu';
|
import {MainMenu} from './main_menu';
|
||||||
@@ -28,24 +28,18 @@ describe('components/Menu', () => {
|
|||||||
// return wrapper.find('MainMenu').shallow();
|
// return wrapper.find('MainMenu').shallow();
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps: ComponentProps<typeof MainMenu> = {
|
||||||
mobile: false,
|
mobile: false,
|
||||||
teamId: 'team-id',
|
teamId: 'team-id',
|
||||||
teamType: Constants.OPEN_TEAM,
|
|
||||||
teamName: 'team_name',
|
teamName: 'team_name',
|
||||||
currentUser: TestHelper.getUserMock(),
|
currentUser: TestHelper.getUserMock(),
|
||||||
appDownloadLink: undefined,
|
appDownloadLink: undefined,
|
||||||
enableCommands: false,
|
enableCommands: false,
|
||||||
enableCustomEmoji: false,
|
|
||||||
enableIncomingWebhooks: false,
|
enableIncomingWebhooks: false,
|
||||||
enableOAuthServiceProvider: false,
|
enableOAuthServiceProvider: false,
|
||||||
enableOutgoingWebhooks: false,
|
enableOutgoingWebhooks: false,
|
||||||
canManageSystemBots: false,
|
canManageSystemBots: false,
|
||||||
canCreateOrDeleteCustomEmoji: false,
|
|
||||||
canManageIntegrations: true,
|
canManageIntegrations: true,
|
||||||
enableUserCreation: false,
|
|
||||||
enableEmailInvitations: false,
|
|
||||||
enablePluginMarketplace: false,
|
|
||||||
experimentalPrimaryTeam: undefined,
|
experimentalPrimaryTeam: undefined,
|
||||||
helpLink: undefined,
|
helpLink: undefined,
|
||||||
reportAProblemLink: undefined,
|
reportAProblemLink: undefined,
|
||||||
@@ -61,13 +55,10 @@ describe('components/Menu', () => {
|
|||||||
showFlaggedPosts: jest.fn(),
|
showFlaggedPosts: jest.fn(),
|
||||||
closeRightHandSide: jest.fn(),
|
closeRightHandSide: jest.fn(),
|
||||||
closeRhsMenu: jest.fn(),
|
closeRhsMenu: jest.fn(),
|
||||||
getCloudLimits: jest.fn(),
|
|
||||||
},
|
},
|
||||||
teamIsGroupConstrained: false,
|
teamIsGroupConstrained: false,
|
||||||
isCloud: false,
|
isCloud: false,
|
||||||
isStarterFree: false,
|
isStarterFree: false,
|
||||||
subscription: {},
|
|
||||||
userIsAdmin: true,
|
|
||||||
isFreeTrial: false,
|
isFreeTrial: false,
|
||||||
usageDeltaTeams: 1,
|
usageDeltaTeams: 1,
|
||||||
};
|
};
|
||||||
@@ -178,23 +169,21 @@ describe('components/Menu', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should match snapshot with plugins', () => {
|
test('should match snapshot with plugins', () => {
|
||||||
const props = {
|
const props: ComponentProps<typeof MainMenu> = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
pluginMenuItems: [{
|
pluginMenuItems: [{
|
||||||
id: 'plugin-id-1',
|
id: 'plugin-id-1',
|
||||||
pluginId: 'plugin-1',
|
pluginId: 'plugin-1',
|
||||||
mobileIcon: <i className='fa fa-anchor'/>,
|
mobileIcon: <i className='fa fa-anchor'/>,
|
||||||
action: jest.fn,
|
action: jest.fn,
|
||||||
dropdownText: 'some dropdown text',
|
text: 'some text',
|
||||||
tooltipText: 'some tooltip text',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'plugind-id-2',
|
id: 'plugind-id-2',
|
||||||
pluginId: 'plugin-2',
|
pluginId: 'plugin-2',
|
||||||
mobileIcon: <i className='fa fa-anchor'/>,
|
mobileIcon: <i className='fa fa-anchor'/>,
|
||||||
action: jest.fn,
|
action: jest.fn,
|
||||||
dropdownText: 'some dropdown text',
|
text: 'some text',
|
||||||
tooltipText: 'some tooltip text',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -203,24 +192,22 @@ describe('components/Menu', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should match snapshot with plugins in mobile', () => {
|
test('should match snapshot with plugins in mobile', () => {
|
||||||
const props = {
|
const props: ComponentProps<typeof MainMenu> = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
mobile: true,
|
mobile: true,
|
||||||
pluginMenuItems: [{
|
pluginMenuItems: [{
|
||||||
id: 'plugin-id-1',
|
id: 'plugin-id-1',
|
||||||
pluginId: 'plugin-1',
|
pluginId: 'plugin-1',
|
||||||
icon: <i className='fa fa-anchor'/>,
|
mobileIcon: <i className='fa fa-anchor'/>,
|
||||||
action: jest.fn,
|
action: jest.fn,
|
||||||
dropdownText: 'some dropdown text',
|
text: 'some text',
|
||||||
tooltipText: 'some tooltip text',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'plugind-id-2',
|
id: 'plugind-id-2',
|
||||||
pluginId: 'plugin-2',
|
pluginId: 'plugin-2',
|
||||||
icon: <i className='fa fa-anchor'/>,
|
mobileIcon: <i className='fa fa-anchor'/>,
|
||||||
action: jest.fn,
|
action: jest.fn,
|
||||||
dropdownText: 'some dropdown text',
|
text: 'some text',
|
||||||
tooltipText: 'some tooltip text',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {makeUrlSafe} from 'utils/url';
|
|||||||
import * as UserAgent from 'utils/user_agent';
|
import * as UserAgent from 'utils/user_agent';
|
||||||
|
|
||||||
import type {ModalData} from 'types/actions';
|
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 LearnAboutTeamsLink from './learn_about_teams_link';
|
||||||
import './main_menu.scss';
|
import './main_menu.scss';
|
||||||
@@ -56,7 +56,7 @@ export type Props = {
|
|||||||
helpLink?: string;
|
helpLink?: string;
|
||||||
reportAProblemLink?: string;
|
reportAProblemLink?: string;
|
||||||
moreTeamsToJoin: boolean;
|
moreTeamsToJoin: boolean;
|
||||||
pluginMenuItems?: PluginComponent[];
|
pluginMenuItems?: MainMenuAction[];
|
||||||
isMentionSearch?: boolean;
|
isMentionSearch?: boolean;
|
||||||
teamIsGroupConstrained: boolean;
|
teamIsGroupConstrained: boolean;
|
||||||
isLicensedForLDAPGroups?: boolean;
|
isLicensedForLDAPGroups?: boolean;
|
||||||
|
|||||||
@@ -74,8 +74,7 @@ const NewChannelModal = () => {
|
|||||||
const [channelInputError, setChannelInputError] = useState(false);
|
const [channelInputError, setChannelInputError] = useState(false);
|
||||||
|
|
||||||
// create a board along with the channel
|
// create a board along with the channel
|
||||||
const pluginsComponentsList = useSelector((state: GlobalState) => state.plugins.components);
|
const createBoardFromChannelPlugin = useSelector((state: GlobalState) => state.plugins.components.CreateBoardFromTemplate);
|
||||||
const createBoardFromChannelPlugin = pluginsComponentsList?.CreateBoardFromTemplate;
|
|
||||||
const newChannelWithBoardPulsatingDotState = useSelector((state: GlobalState) => getPreference(state, Preferences.APP_BAR, Preferences.NEW_CHANNEL_WITH_BOARD_TOUR_SHOWED, ''));
|
const newChannelWithBoardPulsatingDotState = useSelector((state: GlobalState) => getPreference(state, Preferences.APP_BAR, Preferences.NEW_CHANNEL_WITH_BOARD_TOUR_SHOWED, ''));
|
||||||
|
|
||||||
const [canCreateFromPluggable, setCanCreateFromPluggable] = useState(true);
|
const [canCreateFromPluggable, setCanCreateFromPluggable] = useState(true);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const SearchBoxHints = ({searchTerms, setSearchTerms, searchType, providerResult
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Component: any = pluginComponentInfo.component;
|
const Component = pluginComponentInfo.component;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<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) {
|
if (searchType === pluginId) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ const SearchSuggestions = ({searchType, searchTerms, suggestionsHeader, provider
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Component: any = pluginComponentInfo.component;
|
const Component = pluginComponentInfo.component;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default function PluginLinkTooltip(props: Props) {
|
|||||||
{...getFloatingProps()}
|
{...getFloatingProps()}
|
||||||
>
|
>
|
||||||
<Pluggable
|
<Pluggable
|
||||||
href={props.nodeAttributes.href}
|
href={props.nodeAttributes.href || ''}
|
||||||
show={true}
|
show={true}
|
||||||
pluggableName='LinkTooltip'
|
pluggableName='LinkTooltip'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import {isKeyPressed} from 'utils/keyboard';
|
|||||||
import * as PostUtils from 'utils/post_utils';
|
import * as PostUtils from 'utils/post_utils';
|
||||||
import {getDateForUnixTicks, makeIsEligibleForClick} from 'utils/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 PostOptions from './post_options';
|
||||||
import PostUserProfile from './user_profile';
|
import PostUserProfile from './user_profile';
|
||||||
@@ -116,7 +116,7 @@ export type Props = {
|
|||||||
isPostPriorityEnabled: boolean;
|
isPostPriorityEnabled: boolean;
|
||||||
isCardOpen?: boolean;
|
isCardOpen?: boolean;
|
||||||
canDelete?: boolean;
|
canDelete?: boolean;
|
||||||
pluginActions: PluginComponent[];
|
pluginActions: PostActionComponent[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const PostComponent = (props: Props): JSX.Element => {
|
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 {Locations, Constants} from 'utils/constants';
|
||||||
import {isSystemMessage, fromAutoResponder} from 'utils/post_utils';
|
import {isSystemMessage, fromAutoResponder} from 'utils/post_utils';
|
||||||
|
|
||||||
import type {PluginComponent} from 'types/store/plugins';
|
import type {PostActionComponent} from 'types/store/plugins';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
post: Post;
|
post: Post;
|
||||||
@@ -52,7 +52,7 @@ type Props = {
|
|||||||
isPostHeaderVisible?: boolean | null;
|
isPostHeaderVisible?: boolean | null;
|
||||||
isPostBeingEdited?: boolean;
|
isPostBeingEdited?: boolean;
|
||||||
canDelete?: boolean;
|
canDelete?: boolean;
|
||||||
pluginActions: PluginComponent[];
|
pluginActions: PostActionComponent[];
|
||||||
actions: {
|
actions: {
|
||||||
emitShortcutReactToLastPostFrom: (emittedFrom: 'CENTER' | 'RHS_ROOT' | 'NO_WHERE') => void;
|
emitShortcutReactToLastPostFrom: (emittedFrom: 'CENTER' | 'RHS_ROOT' | 'NO_WHERE') => void;
|
||||||
};
|
};
|
||||||
@@ -191,7 +191,7 @@ const PostOptions = (props: Props): JSX.Element => {
|
|||||||
pluginItems = props.pluginActions?.
|
pluginItems = props.pluginActions?.
|
||||||
map((item) => {
|
map((item) => {
|
||||||
if (item.component) {
|
if (item.component) {
|
||||||
const Component = item.component as any;
|
const Component = item.component;
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
post={props.post}
|
post={props.post}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {ComponentProps} from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type {Post, PostType} from '@mattermost/types/posts';
|
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 {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||||
import {TestHelper} from 'utils/test_helper';
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
import type {MessageWillFormatHook} from 'types/store/plugins';
|
|
||||||
|
|
||||||
import PostMarkdown from './post_markdown';
|
import PostMarkdown from './post_markdown';
|
||||||
|
|
||||||
describe('components/PostMarkdown', () => {
|
describe('components/PostMarkdown', () => {
|
||||||
const baseProps = {
|
const baseProps: ComponentProps<typeof PostMarkdown> = {
|
||||||
imageProps: {} as Record<string, unknown>,
|
imageProps: {} as Record<string, unknown>,
|
||||||
pluginHooks: [],
|
pluginHooks: [],
|
||||||
message: 'message',
|
message: 'message',
|
||||||
@@ -211,7 +210,7 @@ describe('components/PostMarkdown', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('plugin hooks can build upon other hook message updates', () => {
|
test('plugin hooks can build upon other hook message updates', () => {
|
||||||
const props = {
|
const props: ComponentProps<typeof PostMarkdown> = {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
message: 'world',
|
message: 'world',
|
||||||
post: TestHelper.getPostMock({
|
post: TestHelper.getPostMock({
|
||||||
@@ -226,16 +225,20 @@ describe('components/PostMarkdown', () => {
|
|||||||
}),
|
}),
|
||||||
pluginHooks: [
|
pluginHooks: [
|
||||||
{
|
{
|
||||||
|
id: 'some id',
|
||||||
|
pluginId: 'some plugin',
|
||||||
hook: (post: Post, updatedMessage: string) => {
|
hook: (post: Post, updatedMessage: string) => {
|
||||||
return 'hello ' + updatedMessage;
|
return 'hello ' + updatedMessage;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'different id',
|
||||||
|
pluginId: 'different plugin',
|
||||||
hook: (post: Post, updatedMessage: string) => {
|
hook: (post: Post, updatedMessage: string) => {
|
||||||
return updatedMessage + '!';
|
return updatedMessage + '!';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
] as MessageWillFormatHook[],
|
],
|
||||||
};
|
};
|
||||||
renderWithContext(<PostMarkdown {...props}/>, state);
|
renderWithContext(<PostMarkdown {...props}/>, state);
|
||||||
expect(screen.queryByText('world', {exact: true})).not.toBeInTheDocument();
|
expect(screen.queryByText('world', {exact: true})).not.toBeInTheDocument();
|
||||||
@@ -245,7 +248,7 @@ describe('components/PostMarkdown', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('plugin hooks can overwrite other hooks messages', () => {
|
test('plugin hooks can overwrite other hooks messages', () => {
|
||||||
const props = {
|
const props: ComponentProps<typeof PostMarkdown> = {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
message: 'world',
|
message: 'world',
|
||||||
post: TestHelper.getPostMock({
|
post: TestHelper.getPostMock({
|
||||||
@@ -260,16 +263,20 @@ describe('components/PostMarkdown', () => {
|
|||||||
}),
|
}),
|
||||||
pluginHooks: [
|
pluginHooks: [
|
||||||
{
|
{
|
||||||
|
id: 'some id',
|
||||||
|
pluginId: 'some plugin',
|
||||||
hook: (post: Post) => {
|
hook: (post: Post) => {
|
||||||
return 'hello ' + post.message;
|
return 'hello ' + post.message;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'different id',
|
||||||
|
pluginId: 'different plugin',
|
||||||
hook: (post: Post) => {
|
hook: (post: Post) => {
|
||||||
return post.message + '!';
|
return post.message + '!';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
] as unknown as MessageWillFormatHook[],
|
],
|
||||||
};
|
};
|
||||||
renderWithContext(<PostMarkdown {...props}/>, state);
|
renderWithContext(<PostMarkdown {...props}/>, state);
|
||||||
expect(screen.queryByText('world', {exact: true})).not.toBeInTheDocument();
|
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 {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||||
|
|
||||||
import type {PluginComponent} from 'types/store/plugins';
|
import type {ChannelIntroButtonAction} from 'types/store/plugins';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
channelMember?: ChannelMembership;
|
channelMember?: ChannelMembership;
|
||||||
pluginButtons: PluginComponent[];
|
pluginButtons: ChannelIntroButtonAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const PluggableIntroButtons = React.memo((props: Props) => {
|
const PluggableIntroButtons = React.memo(({
|
||||||
const channelIsArchived = props.channel.delete_at !== 0;
|
channel,
|
||||||
if (channelIsArchived || props.pluginButtons.length === 0) {
|
pluginButtons,
|
||||||
|
channelMember,
|
||||||
|
}: Props) => {
|
||||||
|
const channelIsArchived = channel.delete_at !== 0;
|
||||||
|
if (channelIsArchived || pluginButtons.length === 0 || !channelMember) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttons = props.pluginButtons.map((buttonProps) => {
|
const buttons = pluginButtons.map((buttonProps) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={buttonProps.id}
|
key={buttonProps.id}
|
||||||
className={'action-button'}
|
className={'action-button'}
|
||||||
onClick={() => buttonProps.action?.(props.channel, props.channelMember)}
|
onClick={() => buttonProps.action?.(channel, channelMember)}
|
||||||
>
|
>
|
||||||
{buttonProps.icon}
|
{buttonProps.icon}
|
||||||
{buttonProps.text}
|
{buttonProps.text}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import * as PostList from 'mattermost-redux/utils/post_list';
|
|||||||
|
|
||||||
import NotificationSeparator from 'components/widgets/separator/notification-separator';
|
import NotificationSeparator from 'components/widgets/separator/notification-separator';
|
||||||
|
|
||||||
import type {PluginComponent} from 'types/store/plugins';
|
import type {NewMessagesSeparatorActionComponent} from 'types/store/plugins';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
separatorId: string;
|
separatorId: string;
|
||||||
wrapperRef?: React.RefObject<HTMLDivElement>;
|
wrapperRef?: React.RefObject<HTMLDivElement>;
|
||||||
newMessagesSeparatorActions: PluginComponent[];
|
newMessagesSeparatorActions: NewMessagesSeparatorActionComponent[];
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
threadId?: string;
|
threadId?: string;
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ const NewMessageSeparator = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Component = item.component as any;
|
const Component = item.component;
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {toggleEmbedVisibility} from 'actions/post_actions';
|
|||||||
import {isEmbedVisible} from 'selectors/posts';
|
import {isEmbedVisible} from 'selectors/posts';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
import type {PostWillRenderEmbedPluginComponent} from 'types/store/plugins';
|
|
||||||
|
|
||||||
import PostBodyAdditionalContent from './post_body_additional_content';
|
import PostBodyAdditionalContent from './post_body_additional_content';
|
||||||
import type {
|
import type {
|
||||||
@@ -21,7 +20,7 @@ import type {
|
|||||||
function mapStateToProps(state: GlobalState, ownProps: Omit<Props, 'appsEnabled' | 'actions'>) {
|
function mapStateToProps(state: GlobalState, ownProps: Omit<Props, 'appsEnabled' | 'actions'>) {
|
||||||
return {
|
return {
|
||||||
isEmbedVisible: isEmbedVisible(state, ownProps.post.id),
|
isEmbedVisible: isEmbedVisible(state, ownProps.post.id),
|
||||||
pluginPostWillRenderEmbedComponents: state.plugins.components.PostWillRenderEmbedComponent as unknown as PostWillRenderEmbedPluginComponent[],
|
pluginPostWillRenderEmbedComponents: state.plugins.components.PostWillRenderEmbedComponent,
|
||||||
appsEnabled: appsEnabled(state),
|
appsEnabled: appsEnabled(state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ import YoutubeVideo from 'components/youtube_video';
|
|||||||
import webSocketClient from 'client/web_websocket_client';
|
import webSocketClient from 'client/web_websocket_client';
|
||||||
import type {TextFormattingOptions} from 'utils/text_formatting';
|
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';
|
import EmbeddedBindings from '../embedded_bindings/embedded_bindings';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
post: Post;
|
post: Post;
|
||||||
pluginPostWillRenderEmbedComponents?: PostWillRenderEmbedPluginComponent[];
|
pluginPostWillRenderEmbedComponents?: PostWillRenderEmbedComponent[];
|
||||||
children?: JSX.Element;
|
children?: JSX.Element;
|
||||||
isEmbedVisible?: boolean;
|
isEmbedVisible?: boolean;
|
||||||
options?: Partial<TextFormattingOptions>;
|
options?: Partial<TextFormattingOptions>;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import NewMessageSeparator from 'components/post_view/new_message_separator/new_
|
|||||||
import {PostListRowListIds, Locations} from 'utils/constants';
|
import {PostListRowListIds, Locations} from 'utils/constants';
|
||||||
import {isIdNotPost} from 'utils/post_utils';
|
import {isIdNotPost} from 'utils/post_utils';
|
||||||
|
|
||||||
import type {PluginComponent} from 'types/store/plugins';
|
import type {NewMessagesSeparatorActionComponent} from 'types/store/plugins';
|
||||||
|
|
||||||
export type PostListRowProps = {
|
export type PostListRowProps = {
|
||||||
listId: string;
|
listId: string;
|
||||||
@@ -59,7 +59,7 @@ export type PostListRowProps = {
|
|||||||
firstInaccessiblePostTime?: number;
|
firstInaccessiblePostTime?: number;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
|
|
||||||
newMessagesSeparatorActions: PluginComponent[];
|
newMessagesSeparatorActions: NewMessagesSeparatorActionComponent[];
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
|
|||||||
@@ -711,9 +711,7 @@ export default class PostList extends React.PureComponent<Props, State> {
|
|||||||
{({height, width}) => (
|
{({height, width}) => (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Pluggable
|
<Pluggable pluggableName='ChannelToast'/>
|
||||||
pluggableName='ChannelToast'
|
|
||||||
/>
|
|
||||||
|
|
||||||
{this.renderToasts(width)}
|
{this.renderToasts(width)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ exports[`components/post_view/PostAttachment should match snapshot 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
onHeightChange={[Function]}
|
onHeightChange={[Function]}
|
||||||
pluggableName="PostMessageAttachment"
|
pluggableName="PostMessageAttachment"
|
||||||
postId="post_id"
|
postId="post_id"
|
||||||
@@ -66,7 +66,7 @@ exports[`components/post_view/PostAttachment should match snapshot, on Show Less
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
onHeightChange={[Function]}
|
onHeightChange={[Function]}
|
||||||
pluggableName="PostMessageAttachment"
|
pluggableName="PostMessageAttachment"
|
||||||
postId="post_id"
|
postId="post_id"
|
||||||
@@ -103,7 +103,7 @@ exports[`components/post_view/PostAttachment should match snapshot, on Show More
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
onHeightChange={[Function]}
|
onHeightChange={[Function]}
|
||||||
pluggableName="PostMessageAttachment"
|
pluggableName="PostMessageAttachment"
|
||||||
postId="post_id"
|
postId="post_id"
|
||||||
@@ -159,7 +159,7 @@ exports[`components/post_view/PostAttachment should match snapshot, on edited po
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
onHeightChange={[Function]}
|
onHeightChange={[Function]}
|
||||||
pluggableName="PostMessageAttachment"
|
pluggableName="PostMessageAttachment"
|
||||||
postId="post_id"
|
postId="post_id"
|
||||||
@@ -197,7 +197,7 @@ exports[`components/post_view/PostAttachment should match snapshot, on ephemeral
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
onHeightChange={[Function]}
|
onHeightChange={[Function]}
|
||||||
pluggableName="PostMessageAttachment"
|
pluggableName="PostMessageAttachment"
|
||||||
postId="post_id"
|
postId="post_id"
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import Pluggable from 'plugins/pluggable';
|
|||||||
import type {TextFormattingOptions} from 'utils/text_formatting';
|
import type {TextFormattingOptions} from 'utils/text_formatting';
|
||||||
import * as Utils from 'utils/utils';
|
import * as Utils from 'utils/utils';
|
||||||
|
|
||||||
|
import type {PostPluginComponent} from 'types/store/plugins';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
post: Post; /* The post to render the message for */
|
post: Post; /* The post to render the message for */
|
||||||
enableFormatting?: boolean; /* Set to enable Markdown formatting */
|
enableFormatting?: boolean; /* Set to enable Markdown formatting */
|
||||||
@@ -27,7 +29,9 @@ type Props = {
|
|||||||
isRHSOpen?: boolean; /* Whether or not the RHS is visible */
|
isRHSOpen?: boolean; /* Whether or not the RHS is visible */
|
||||||
isRHSExpanded?: boolean; /* Whether or not the RHS is expanded */
|
isRHSExpanded?: boolean; /* Whether or not the RHS is expanded */
|
||||||
theme: Theme; /* Logged in user's theme */
|
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;
|
currentRelativeTeamUrl: string;
|
||||||
overflowType?: AttachmentTextOverflowType;
|
overflowType?: AttachmentTextOverflowType;
|
||||||
maxHeight?: number; /* The max height used by the show more component */
|
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 {Constants} from 'utils/constants';
|
||||||
|
|
||||||
import type {PluginComponent} from 'types/store/plugins';
|
import type {CallButtonAction} from 'types/store/plugins';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
channelMember?: ChannelMembership;
|
channelMember?: ChannelMembership;
|
||||||
pluginCallComponents: PluginComponent[];
|
pluginCallComponents: CallButtonAction[];
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {Preferences} from 'mattermost-redux/constants';
|
|||||||
import {measurePageLoadTelemetry, temporarilySetPageLoadContext, trackEvent, trackSelectorMetrics} from 'actions/telemetry_actions.jsx';
|
import {measurePageLoadTelemetry, temporarilySetPageLoadContext, trackEvent, trackSelectorMetrics} from 'actions/telemetry_actions.jsx';
|
||||||
import BrowserStore from 'stores/browser_store';
|
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 GlobalHeader from 'components/global_header/global_header';
|
||||||
import {HFRoute} from 'components/header_footer_route/header_footer_route';
|
import {HFRoute} from 'components/header_footer_route/header_footer_route';
|
||||||
import {HFTRoute, LoggedInHFTRoute} from 'components/header_footer_template_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 CreateTeam = makeAsyncComponent('CreateTeam', lazy(() => import('components/create_team')));
|
||||||
const Mfa = makeAsyncComponent('Mfa', lazy(() => import('components/mfa/mfa_controller')));
|
const Mfa = makeAsyncComponent('Mfa', lazy(() => import('components/mfa/mfa_controller')));
|
||||||
const PreparingWorkspace = makeAsyncComponent('PreparingWorkspace', lazy(() => import('components/preparing_workspace')));
|
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 LaunchingWorkspace = makeAsyncComponent('LaunchingWorkspace', lazy(() => import('components/preparing_workspace/launching_workspace')));
|
||||||
const CompassThemeProvider = makeAsyncComponent('CompassThemeProvider', lazy(() => import('components/compass_theme_provider/compass_theme_provider')));
|
const CompassThemeProvider = makeAsyncComponent('CompassThemeProvider', lazy(() => import('components/compass_theme_provider/compass_theme_provider')));
|
||||||
const TeamController = makeAsyncComponent('TeamController', lazy(() => import('components/team_controller')));
|
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 AppBar = makeAsyncComponent('AppBar', lazy(() => import('components/app_bar/app_bar')));
|
||||||
const ComponentLibrary = makeAsyncComponent('ComponentLibrary', lazy(() => import('components/component_library')));
|
const ComponentLibrary = makeAsyncComponent('ComponentLibrary', lazy(() => import('components/component_library')));
|
||||||
|
|
||||||
|
const Pluggable = makeAsyncPluggableComponent();
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
export type Props = PropsFromRedux & RouteComponentProps;
|
export type Props = PropsFromRedux & RouteComponentProps;
|
||||||
@@ -558,7 +559,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
{this.props.plugins?.map((plugin) => (
|
{this.props.plugins?.map((plugin) => (
|
||||||
<Route
|
<Route
|
||||||
key={plugin.id}
|
key={plugin.id}
|
||||||
path={'/plug/' + (plugin as any).route}
|
path={'/plug/' + plugin.route}
|
||||||
render={() => (
|
render={() => (
|
||||||
<Pluggable
|
<Pluggable
|
||||||
pluggableName={'CustomRouteComponent'}
|
pluggableName={'CustomRouteComponent'}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ exports[`components/sidebar should match snapshot 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="sidebar--left__icons"
|
className="sidebar--left__icons"
|
||||||
>
|
>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
pluggableName="LeftSidebarHeader"
|
pluggableName="LeftSidebarHeader"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +75,7 @@ exports[`components/sidebar should match snapshot when direct channels modal is
|
|||||||
<div
|
<div
|
||||||
className="sidebar--left__icons"
|
className="sidebar--left__icons"
|
||||||
>
|
>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
pluggableName="LeftSidebarHeader"
|
pluggableName="LeftSidebarHeader"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +122,7 @@ exports[`components/sidebar should match snapshot when more channels modal is op
|
|||||||
<div
|
<div
|
||||||
className="sidebar--left__icons"
|
className="sidebar--left__icons"
|
||||||
>
|
>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
pluggableName="LeftSidebarHeader"
|
pluggableName="LeftSidebarHeader"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn
|
|||||||
>
|
>
|
||||||
channel_label
|
channel_label
|
||||||
</span>
|
</span>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
channel={
|
channel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -103,7 +103,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn
|
|||||||
>
|
>
|
||||||
channel_label
|
channel_label
|
||||||
</span>
|
</span>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
channel={
|
channel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -189,7 +189,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn
|
|||||||
channel_label
|
channel_label
|
||||||
</span>
|
</span>
|
||||||
</WithTooltip>
|
</WithTooltip>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
channel={
|
channel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -271,7 +271,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_link should match sn
|
|||||||
>
|
>
|
||||||
channel_label
|
channel_label
|
||||||
</span>
|
</span>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
channel={
|
channel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
|
|||||||
import {reconnect} from 'actions/websocket_actions.jsx';
|
import {reconnect} from 'actions/websocket_actions.jsx';
|
||||||
import LocalStorageStore from 'stores/local_storage_store';
|
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 ChannelController from 'components/channel_layout/channel_controller';
|
||||||
import useTelemetryIdentitySync from 'components/common/hooks/useTelemetryIdentifySync';
|
import useTelemetryIdentitySync from 'components/common/hooks/useTelemetryIdentifySync';
|
||||||
import InitialLoadingScreen from 'components/initial_loading_screen';
|
import InitialLoadingScreen from 'components/initial_loading_screen';
|
||||||
@@ -27,7 +27,7 @@ import {isIosSafari} from 'utils/user_agent';
|
|||||||
import type {OwnProps, PropsFromRedux} from './index';
|
import type {OwnProps, PropsFromRedux} from './index';
|
||||||
|
|
||||||
const BackstageController = makeAsyncComponent('BackstageController', lazy(() => import('components/backstage')));
|
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_CHECK_INTERVAL = 30000; // 30 seconds
|
||||||
const WAKEUP_THRESHOLD = 60000; // 60 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 {Locations} from 'utils/constants';
|
||||||
|
|
||||||
import type {PluginComponent} from 'types/store/plugins';
|
import type {NewMessagesSeparatorActionComponent} from 'types/store/plugins';
|
||||||
|
|
||||||
import Reply from './reply';
|
import Reply from './reply';
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ type Props = {
|
|||||||
previousPostId: string;
|
previousPostId: string;
|
||||||
timestampProps?: Partial<TimestampProps>;
|
timestampProps?: Partial<TimestampProps>;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
newMessagesSeparatorActions: PluginComponent[];
|
newMessagesSeparatorActions: NewMessagesSeparatorActionComponent[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function noop() {}
|
function noop() {}
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ import DelayedAction from 'utils/delayed_action';
|
|||||||
import {getPreviousPostId, getLatestPostId} from 'utils/post_utils';
|
import {getPreviousPostId, getLatestPostId} from 'utils/post_utils';
|
||||||
import * as Utils from 'utils/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 type {FakePost} from 'types/store/rhs';
|
||||||
|
|
||||||
import CreateComment from './create_comment';
|
import CreateComment from './create_comment';
|
||||||
import Row from './thread_viewer_row';
|
import Row from './thread_viewer_row';
|
||||||
|
|
||||||
import './virtualized_thread_viewer.scss';
|
import './virtualized_thread_viewer.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -40,7 +41,7 @@ type Props = {
|
|||||||
useRelativeTimestamp: boolean;
|
useRelativeTimestamp: boolean;
|
||||||
isMobileView: boolean;
|
isMobileView: boolean;
|
||||||
isThreadView: boolean;
|
isThreadView: boolean;
|
||||||
newMessagesSeparatorActions: PluginComponent[];
|
newMessagesSeparatorActions: NewMessagesSeparatorActionComponent[];
|
||||||
inputPlaceholder?: string;
|
inputPlaceholder?: string;
|
||||||
measureRhsOpened: () => void;
|
measureRhsOpened: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export type Props = WrappedComponentProps & {
|
|||||||
subMenu?: Menu[];
|
subMenu?: Menu[];
|
||||||
subMenuClass?: string;
|
subMenuClass?: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
action?: (id?: string) => void;
|
action?: (id: string) => void;
|
||||||
filter?: (id?: string) => boolean;
|
filter?: (id: string) => boolean;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
root?: boolean;
|
root?: boolean;
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
@@ -111,14 +111,14 @@ export class SubMenuItem extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
showMobileSubMenuModal(subMenu);
|
showMobileSubMenuModal(subMenu);
|
||||||
} else if (action) { // leaf node in the tree handles action only
|
} else if (action) { // leaf node in the tree handles action only
|
||||||
action(postId);
|
action(postId || '');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const shouldCallAction =
|
const shouldCallAction =
|
||||||
(event.type === 'keydown' && event.currentTarget.id === id) ||
|
(event.type === 'keydown' && event.currentTarget.id === id) ||
|
||||||
event.target.parentElement.id === id;
|
event.target.parentElement.id === id;
|
||||||
if (shouldCallAction && action) {
|
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 {id, postId, text, selectedValueText, subMenu, icon, filter, ariaLabel, direction, styleSelectableItem, extraText, renderSelected, rightDecorator, tabIndex, intl} = this.props;
|
||||||
const isMobile = isMobileViewHack();
|
const isMobile = isMobileViewHack();
|
||||||
|
|
||||||
if (filter && !filter(id)) {
|
if (filter && !filter(id || '')) {
|
||||||
return ('');
|
return ('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
|||||||
|
|
||||||
import {Constants} from 'utils/constants';
|
import {Constants} from 'utils/constants';
|
||||||
|
|
||||||
import type {PluginComponent} from 'types/store/plugins';
|
import type {CallButtonAction} from 'types/store/plugins';
|
||||||
|
|
||||||
import './call_button.scss';
|
import './call_button.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
currentChannel?: Channel;
|
currentChannel?: Channel;
|
||||||
channelMember?: ChannelMembership;
|
channelMember?: ChannelMembership;
|
||||||
pluginCallComponents: PluginComponent[];
|
pluginCallComponents: CallButtonAction[];
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import React from 'react';
|
|||||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||||
import {TestHelper} from 'utils/test_helper';
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
import type {PluginComponent} from 'types/store/plugins';
|
|
||||||
|
|
||||||
import ChannelHeaderPlug, {maxComponentsBeforeDropdown} from './channel_header_plug';
|
import ChannelHeaderPlug, {maxComponentsBeforeDropdown} from './channel_header_plug';
|
||||||
|
|
||||||
describe('plugins/ChannelHeaderPlug', () => {
|
describe('plugins/ChannelHeaderPlug', () => {
|
||||||
@@ -26,7 +24,7 @@ describe('plugins/ChannelHeaderPlug', () => {
|
|||||||
shouldShowAppBar: false,
|
shouldShowAppBar: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function makeTestPlug(n = 1): PluginComponent {
|
function makeTestPlug(n = 1) {
|
||||||
return {
|
return {
|
||||||
id: 'someid' + n,
|
id: 'someid' + n,
|
||||||
pluginId: 'pluginid' + n,
|
pluginId: 'pluginid' + n,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {createCallContext} from 'utils/apps';
|
|||||||
import {Constants} from 'utils/constants';
|
import {Constants} from 'utils/constants';
|
||||||
|
|
||||||
import type {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForChannel} from 'types/apps';
|
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 = {
|
type CustomMenuProps = {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
@@ -100,7 +100,7 @@ class CustomToggle extends React.PureComponent<CustomToggleProps> {
|
|||||||
|
|
||||||
type ChannelHeaderPlugProps = {
|
type ChannelHeaderPlugProps = {
|
||||||
intl: IntlShape;
|
intl: IntlShape;
|
||||||
components: PluginComponent[];
|
components: ChannelHeaderButtonAction[];
|
||||||
appBindings?: AppBinding[];
|
appBindings?: AppBinding[];
|
||||||
appsEnabled: boolean;
|
appsEnabled: boolean;
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
@@ -164,16 +164,20 @@ class ChannelHeaderPlug extends React.PureComponent<ChannelHeaderPlugProps, Chan
|
|||||||
this.onClose();
|
this.onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
createComponentButton = (plug: PluginComponent) => {
|
createComponentButton = (plug: ChannelHeaderButtonAction) => {
|
||||||
// These values are supposed to be strings based on PluginComponent, but some plugins pass non-strings,
|
// 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!
|
// 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') {
|
if (typeof stringOrElement === 'string') {
|
||||||
// This is the case that we hope for
|
// This is the case that we hope for
|
||||||
return stringOrElement;
|
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
|
// This is a FormattedMessage, so extract the props to translate the text manually
|
||||||
return intl.formatMessage(
|
return intl.formatMessage(
|
||||||
{
|
{
|
||||||
@@ -194,13 +198,17 @@ class ChannelHeaderPlug extends React.PureComponent<ChannelHeaderPlugProps, Chan
|
|||||||
ariaLabel = tooltipToAriaLabelHack(this.props.intl, plug.dropdownText);
|
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 (
|
return (
|
||||||
<HeaderIconWrapper
|
<HeaderIconWrapper
|
||||||
key={'channelHeaderButton' + plug.id}
|
key={'channelHeaderButton' + plug.id}
|
||||||
buttonClass='channel-header__icon'
|
buttonClass='channel-header__icon'
|
||||||
onClick={() => this.fireAction(plug.action!)}
|
onClick={() => this.fireAction(plug.action!)}
|
||||||
buttonId={plug.id + 'ChannelHeaderButton'}
|
buttonId={plug.id + 'ChannelHeaderButton'}
|
||||||
tooltip={plug.tooltipText ?? plug.dropdownText ?? ''}
|
tooltip={tooltipText}
|
||||||
ariaLabelOverride={ariaLabel}
|
ariaLabelOverride={ariaLabel}
|
||||||
pluginId={plug.pluginId}
|
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) => {
|
const componentItems = plugs.filter((plug) => plug.action).map((plug) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
|||||||
import {createCallContext} from 'utils/apps';
|
import {createCallContext} from 'utils/apps';
|
||||||
|
|
||||||
import type {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForChannel} from 'types/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 = {
|
type Props = {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Components or actions to add as channel header buttons
|
* Components or actions to add as channel header buttons
|
||||||
*/
|
*/
|
||||||
components?: PluginComponent[];
|
components?: MobileChannelHeaderButtonAction[];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set to true if the plug is in the dropdown
|
* Set to true if the plug is in the dropdown
|
||||||
@@ -84,7 +84,7 @@ class MobileChannelHeaderPlug extends React.PureComponent<Props> {
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
createButton = (plug: PluginComponent) => {
|
createButton = (plug: MobileChannelHeaderButtonAction) => {
|
||||||
const onClick = () => this.fireAction(plug);
|
const onClick = () => this.fireAction(plug);
|
||||||
|
|
||||||
if (this.props.isDropdown) {
|
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);
|
return plugs.map(this.createButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ class MobileChannelHeaderPlug extends React.PureComponent<Props> {
|
|||||||
return bindings.map(this.createAppButton);
|
return bindings.map(this.createAppButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
fireAction(plug: PluginComponent) {
|
fireAction(plug: MobileChannelHeaderButtonAction) {
|
||||||
return plug.action?.(this.props.channel, this.props.channelMember);
|
return plug.action?.(this.props.channel, this.props.channelMember);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,692 +1,23 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`plugins/Pluggable should match snapshot with extended component 1`] = `
|
exports[`plugins/Pluggable should match snapshot with extended component 1`] = `
|
||||||
<Pluggable
|
<div>
|
||||||
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
|
<span
|
||||||
id="pluginId"
|
data-testid="pluginId"
|
||||||
>
|
>
|
||||||
ProfilePopoverPlugin
|
ProfilePopoverPlugin
|
||||||
</span>
|
</span>
|
||||||
</ProfilePopoverPlugin>
|
</div>
|
||||||
</PluggableErrorBoundary>
|
|
||||||
</Pluggable>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`plugins/Pluggable should match snapshot with extended component with pluggableName 1`] = `
|
exports[`plugins/Pluggable should match snapshot with non-null pluggableId 1`] = `<div />`;
|
||||||
<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 valid pluggableId 1`] = `
|
exports[`plugins/Pluggable should match snapshot with valid pluggableId 1`] = `
|
||||||
<Pluggable
|
<div>
|
||||||
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=""
|
|
||||||
>
|
|
||||||
<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
|
<span
|
||||||
id="pluginId"
|
data-testid="pluginId"
|
||||||
>
|
>
|
||||||
ProfilePopoverPlugin
|
ProfilePopoverPlugin
|
||||||
</span>
|
</span>
|
||||||
</ProfilePopoverPlugin>
|
</div>
|
||||||
</PluggableErrorBoundary>
|
|
||||||
</Pluggable>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,19 +1,6 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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';
|
import Pluggable from './pluggable';
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
export default Pluggable;
|
||||||
return {
|
|
||||||
components: state.plugins.components,
|
|
||||||
theme: getTheme(state),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Pluggable);
|
|
||||||
|
|||||||
@@ -3,150 +3,116 @@
|
|||||||
|
|
||||||
import type {ComponentProps} from 'react';
|
import type {ComponentProps} from 'react';
|
||||||
import React 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 {
|
import Pluggable from '.';
|
||||||
render() {
|
|
||||||
return <span id='pluginId'>{'ProfilePopoverPlugin'}</span>;
|
const ProfilePopoverPlugin: React.FunctionComponent = () => (<span data-testid='pluginId'>{'ProfilePopoverPlugin'}</span>);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jest.mock('actions/views/profile_popover');
|
jest.mock('actions/views/profile_popover');
|
||||||
|
|
||||||
describe('plugins/Pluggable', () => {
|
describe('plugins/Pluggable', () => {
|
||||||
const baseProps: ComponentProps<typeof Pluggable> = {
|
const baseProps: ComponentProps<typeof Pluggable> = {
|
||||||
pluggableName: '',
|
pluggableName: 'RightHandSidebarComponent',
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
function getBaseState(): DeepPartial<GlobalState> {
|
||||||
test('should match snapshot with no extended component', () => {
|
return {
|
||||||
const wrapper = mountWithIntl(
|
plugins: {
|
||||||
<Pluggable
|
components: {
|
||||||
{...baseProps}
|
RightHandSidebarComponent: [{
|
||||||
/>,
|
component: ProfilePopoverPlugin,
|
||||||
);
|
id: 'some id',
|
||||||
|
pluginId: 'some plugin id',
|
||||||
expect(wrapper).toMatchSnapshot();
|
}],
|
||||||
});
|
},
|
||||||
|
},
|
||||||
|
entities: {
|
||||||
|
teams: {
|
||||||
|
currentTeamId: '',
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
myPreferences: {},
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
test('should match snapshot with extended component', () => {
|
test('should match snapshot with extended component', () => {
|
||||||
const wrapper = mountWithIntl(
|
const state = getBaseState();
|
||||||
|
const {container} = renderWithContext(
|
||||||
<Pluggable
|
<Pluggable
|
||||||
{...baseProps}
|
{...baseProps}
|
||||||
pluggableName='PopoverSection1'
|
pluggableName='RightHandSidebarComponent'
|
||||||
components={{...baseProps.components, PopoverSection1: [{id: '', pluginId: '', component: ProfilePopoverPlugin}]}}
|
|
||||||
/>,
|
/>,
|
||||||
|
state,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
expect(wrapper.find('#pluginId').text()).toBe('ProfilePopoverPlugin');
|
expect(screen.getByTestId('pluginId')).toBeInTheDocument();
|
||||||
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(true);
|
expect(screen.getByText('ProfilePopoverPlugin')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should match snapshot with extended component with pluggableName', () => {
|
test('should return null if with pluggableName but no components', () => {
|
||||||
const wrapper = mountWithIntl(
|
const state = getBaseState();
|
||||||
|
state.plugins!.components!.RightHandSidebarComponent = [];
|
||||||
|
const store = testConfigureStore(state);
|
||||||
|
const {container} = renderWithContext(
|
||||||
|
<Provider store={store}>
|
||||||
<Pluggable
|
<Pluggable
|
||||||
{...baseProps}
|
{...baseProps}
|
||||||
pluggableName='PopoverSection1'
|
pluggableName='RightHandSidebarComponent'
|
||||||
components={{...baseProps.components, PopoverSection1: [{id: '', pluginId: '', component: ProfilePopoverPlugin}]}}
|
/>
|
||||||
/>,
|
</Provider>,
|
||||||
|
state,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(container).toBeEmptyDOMElement();
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should match snapshot with non-null pluggableId', () => {
|
test('should match snapshot with non-null pluggableId', () => {
|
||||||
const wrapper = mountWithIntl(
|
const state = getBaseState();
|
||||||
|
const store = testConfigureStore(state);
|
||||||
|
const {container} = renderWithContext(
|
||||||
|
<Provider store={store}>
|
||||||
<Pluggable
|
<Pluggable
|
||||||
{...baseProps}
|
{...baseProps}
|
||||||
pluggableName='PopoverSection1'
|
pluggableName='RightHandSidebarComponent'
|
||||||
pluggableId={'pluggableId'}
|
pluggableId={'pluggableId'}
|
||||||
components={{...baseProps.components, PopoverSection1: [{id: '', pluginId: '', component: ProfilePopoverPlugin}]}}
|
/>
|
||||||
/>,
|
</Provider>,
|
||||||
|
state,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(false);
|
expect(container).toBeEmptyDOMElement();
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should match snapshot with valid pluggableId', () => {
|
test('should match snapshot with valid pluggableId', () => {
|
||||||
const wrapper = mountWithIntl(
|
const state = getBaseState();
|
||||||
|
state.plugins!.components!.RightHandSidebarComponent![0]!.id = 'pluggableId';
|
||||||
|
const store = testConfigureStore(state);
|
||||||
|
const {container} = renderWithContext(
|
||||||
|
<Provider store={store}>
|
||||||
<Pluggable
|
<Pluggable
|
||||||
{...baseProps}
|
{...baseProps}
|
||||||
pluggableName='PopoverSection1'
|
pluggableName='RightHandSidebarComponent'
|
||||||
pluggableId={'pluggableId'}
|
pluggableId={'pluggableId'}
|
||||||
components={{...baseProps.components, PopoverSection1: [{id: 'pluggableId', pluginId: '', component: ProfilePopoverPlugin}]}}
|
/>
|
||||||
/>,
|
</Provider>,
|
||||||
|
state,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(true);
|
expect(screen.getByTestId('pluginId')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,34 +2,29 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
import type {WebSocketClient} from '@mattermost/client';
|
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
|
||||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
|
||||||
|
|
||||||
import webSocketClient from 'client/web_websocket_client';
|
import webSocketClient from 'client/web_websocket_client';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
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';
|
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
|
* Override the component to be plugged
|
||||||
*/
|
*/
|
||||||
pluggableName: string;
|
pluggableName: T;
|
||||||
|
|
||||||
/*
|
|
||||||
* Components for overriding provided by plugins
|
|
||||||
*/
|
|
||||||
components: GlobalState['plugins']['components'];
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Logged in user's theme
|
|
||||||
*/
|
|
||||||
theme: Theme;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Id of the specific component to be plugged.
|
* Id of the specific component to be plugged.
|
||||||
@@ -41,35 +36,33 @@ type Props = {
|
|||||||
*
|
*
|
||||||
* Only supported when pluggableName is "Product".
|
* Only supported when pluggableName is "Product".
|
||||||
*/
|
*/
|
||||||
subComponentName?: 'mainComponent' | 'publicComponent' | 'headerCentreComponent' | 'headerRightComponent';
|
subComponentName?: U;
|
||||||
|
|
||||||
/*
|
|
||||||
* Accept any other prop to pass onto the plugin component
|
|
||||||
*/
|
|
||||||
[name: string]: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseChildProps = {
|
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')>
|
||||||
theme: Theme;
|
|
||||||
webSocketClient?: 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 {
|
const {
|
||||||
components,
|
|
||||||
pluggableId,
|
pluggableId,
|
||||||
pluggableName,
|
pluggableName,
|
||||||
subComponentName = '',
|
subComponentName = '',
|
||||||
theme,
|
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pluginComponents = components[pluggableName]!;
|
let pluginComponents: PluggableType[] = [...allPluginComponents];
|
||||||
|
|
||||||
if (pluggableId) {
|
if (pluggableId) {
|
||||||
pluginComponents = pluginComponents.filter(
|
pluginComponents = pluginComponents.filter(
|
||||||
(element) => element.id === pluggableId);
|
(element) => element.id === pluggableId);
|
||||||
@@ -80,12 +73,16 @@ export default function Pluggable(props: Props): JSX.Element | null {
|
|||||||
let content;
|
let content;
|
||||||
|
|
||||||
if (pluggableName === 'Product') {
|
if (pluggableName === 'Product') {
|
||||||
content = (pluginComponents as ProductComponent[]).map((pc) => {
|
const productComponents = pluginComponents as ProductComponent[];
|
||||||
|
content = (productComponents).map((pc) => {
|
||||||
if (!subComponentName || !pc[subComponentName]) {
|
if (!subComponentName || !pc[subComponentName]) {
|
||||||
return null;
|
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 (
|
return (
|
||||||
<PluggableErrorBoundary
|
<PluggableErrorBoundary
|
||||||
@@ -101,11 +98,14 @@ export default function Pluggable(props: Props): JSX.Element | null {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
content = pluginComponents.map((p) => {
|
content = pluginComponents.map((p) => {
|
||||||
if (!p.component) {
|
if (!('component' in p) || !p.component) {
|
||||||
return null;
|
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 (
|
return (
|
||||||
<PluggableErrorBoundary
|
<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 {isValidElementType} from 'react-is';
|
||||||
import type {Reducer} from 'redux';
|
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 reducerRegistry from 'mattermost-redux/store/reducer_registry';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -35,13 +31,43 @@ import {ActionTypes} from 'utils/constants';
|
|||||||
import {reArg} from 'utils/func';
|
import {reArg} from 'utils/func';
|
||||||
import {generateId} from 'utils/utils';
|
import {generateId} from 'utils/utils';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {
|
||||||
import type {PluginComponent, PluginsState, ProductComponent, NeedsTeamComponent} from 'types/store/plugins';
|
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;
|
const defaultShouldRender = () => true;
|
||||||
|
|
||||||
type DPluginComponentProp = {component: PluginComponent['component']};
|
type DPluginComponentProp = {component: React.ComponentType<unknown>};
|
||||||
function dispatchPluginComponentAction(name: keyof PluginsState['components'], pluginId: string, component: PluginComponent['component'], id = generateId()) {
|
function dispatchPluginComponentAction(name: keyof PluginsState['components'], pluginId: string, component: React.ComponentType<any>, id = generateId()) {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
||||||
name,
|
name,
|
||||||
@@ -55,6 +81,14 @@ function dispatchPluginComponentAction(name: keyof PluginsState['components'], p
|
|||||||
return id;
|
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;
|
type ReactResolvable = React.ReactNode | React.ElementType;
|
||||||
const resolveReactElement = (element: ReactResolvable) => {
|
const resolveReactElement = (element: ReactResolvable) => {
|
||||||
if (
|
if (
|
||||||
@@ -128,17 +162,23 @@ export default class PluginRegistry {
|
|||||||
|
|
||||||
// Register components for search.
|
// Register components for search.
|
||||||
// Accepts React components. Returns a unique identifier.
|
// 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();
|
const id = generateId();
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('SearchButtons', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'SearchButtons',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
component: buttonComponent,
|
component: buttonComponent,
|
||||||
action,
|
action,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
dispatchPluginComponentAction('SearchSuggestions', this.id, suggestionsComponent, id);
|
dispatchPluginComponentAction('SearchSuggestions', this.id, suggestionsComponent, id);
|
||||||
dispatchPluginComponentAction('SearchHints', this.id, hintsComponent, 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
|
// 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
|
// the channel has been succesfully created
|
||||||
// Accepts a React component. Returns a unique identifier.
|
// 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();
|
const id = generateId();
|
||||||
|
dispatchPluginComponentWithData('CreateBoardFromTemplate', {
|
||||||
store.dispatch({
|
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'CreateBoardFromTemplate',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
component,
|
component,
|
||||||
action,
|
action,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,7 +233,7 @@ export default class PluginRegistry {
|
|||||||
tooltipText,
|
tooltipText,
|
||||||
}: {
|
}: {
|
||||||
icon: ReactResolvable;
|
icon: ReactResolvable;
|
||||||
action: PluginComponent['action'];
|
action: ChannelHeaderButtonAction['action'];
|
||||||
dropdownText: ReactResolvable;
|
dropdownText: ReactResolvable;
|
||||||
tooltipText: ReactResolvable;
|
tooltipText: ReactResolvable;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -208,17 +248,8 @@ export default class PluginRegistry {
|
|||||||
tooltipText: resolveReactElement(tooltipText),
|
tooltipText: resolveReactElement(tooltipText),
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('ChannelHeaderButton', data);
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
dispatchPluginComponentWithData('MobileChannelHeaderButton', data);
|
||||||
name: 'ChannelHeaderButton',
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
store.dispatch({
|
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'MobileChannelHeaderButton',
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
});
|
});
|
||||||
@@ -238,7 +269,7 @@ export default class PluginRegistry {
|
|||||||
text,
|
text,
|
||||||
}: {
|
}: {
|
||||||
icon: ReactResolvable;
|
icon: ReactResolvable;
|
||||||
action: PluginComponent['action'];
|
action: ChannelIntroButtonAction['action'];
|
||||||
text: ReactResolvable;
|
text: ReactResolvable;
|
||||||
}) => {
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
@@ -251,11 +282,7 @@ export default class PluginRegistry {
|
|||||||
text,
|
text,
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('ChannelIntroButton', data);
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'ChannelIntroButton',
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
});
|
});
|
||||||
@@ -279,7 +306,7 @@ export default class PluginRegistry {
|
|||||||
}: {
|
}: {
|
||||||
button: ReactResolvable;
|
button: ReactResolvable;
|
||||||
dropdownButton: ReactResolvable;
|
dropdownButton: ReactResolvable;
|
||||||
action: (currentChannel: Channel, myCurrentChannelMembership: ChannelMembership) => void;
|
action: CallButtonAction['action'];
|
||||||
}) => {
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
@@ -288,20 +315,12 @@ export default class PluginRegistry {
|
|||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
button: resolveReactElement(button),
|
button: resolveReactElement(button),
|
||||||
dropdownButton: resolveReactElement(dropdownButton),
|
dropdownButton: resolveReactElement(dropdownButton),
|
||||||
|
icon: null, // Needed to satisfy types for MobileChannelHeaderButton
|
||||||
action,
|
action,
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('CallButton', data);
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
dispatchPluginComponentWithData('MobileChannelHeaderButton', data);
|
||||||
name: 'CallButton',
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
store.dispatch({
|
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'MobileChannelHeaderButton',
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
});
|
});
|
||||||
@@ -357,19 +376,23 @@ export default class PluginRegistry {
|
|||||||
// - component - The component that renders the embed view for the link
|
// - component - The component that renders the embed view for the link
|
||||||
// - toggleable - A boolean indicating if the embed view should be collapsable
|
// - toggleable - A boolean indicating if the embed view should be collapsable
|
||||||
// Returns a unique identifier.
|
// 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();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('PostWillRenderEmbedComponent', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'PostWillRenderEmbedComponent',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
component,
|
component,
|
||||||
match,
|
match,
|
||||||
toggleable,
|
toggleable,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -391,21 +414,17 @@ export default class PluginRegistry {
|
|||||||
mobileIcon,
|
mobileIcon,
|
||||||
}: {
|
}: {
|
||||||
text: ReactResolvable;
|
text: ReactResolvable;
|
||||||
action: PluginComponent['action'];
|
action: MainMenuAction['action'];
|
||||||
mobileIcon: ReactResolvable;
|
mobileIcon: ReactResolvable;
|
||||||
}) => {
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('MainMenu', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'MainMenu',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
text: resolveReactElement(text),
|
text: resolveReactElement(text),
|
||||||
action,
|
action,
|
||||||
mobileIcon: resolveReactElement(mobileIcon),
|
mobileIcon: resolveReactElement(mobileIcon),
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -427,22 +446,18 @@ export default class PluginRegistry {
|
|||||||
action,
|
action,
|
||||||
shouldRender = defaultShouldRender,
|
shouldRender = defaultShouldRender,
|
||||||
}: {
|
}: {
|
||||||
text: ReactResolvable;
|
text: ChannelHeaderAction['text'];
|
||||||
action: PluginComponent['action'];
|
action: ChannelHeaderAction['action'];
|
||||||
shouldRender?: (state: GlobalState) => boolean;
|
shouldRender?: ChannelHeaderAction['shouldRender'];
|
||||||
}) => {
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('ChannelHeader', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'ChannelHeader',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
text: resolveReactElement(text),
|
text: resolveReactElement(text),
|
||||||
action,
|
action,
|
||||||
shouldRender,
|
shouldRender,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -463,22 +478,18 @@ export default class PluginRegistry {
|
|||||||
text,
|
text,
|
||||||
action,
|
action,
|
||||||
}: {
|
}: {
|
||||||
match: (fileInfo: FileInfo) => boolean;
|
match: FilesDropdownAction['match'];
|
||||||
text: ReactResolvable;
|
text: ReactResolvable;
|
||||||
action: PluginComponent['action'];
|
action: FilesDropdownAction['action'];
|
||||||
}) => {
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('FilesDropdown', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'FilesDropdown',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
match,
|
match,
|
||||||
text: resolveReactElement(text),
|
text: resolveReactElement(text),
|
||||||
action,
|
action,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -497,19 +508,15 @@ export default class PluginRegistry {
|
|||||||
action,
|
action,
|
||||||
}: {
|
}: {
|
||||||
text: ReactResolvable;
|
text: ReactResolvable;
|
||||||
action: PluginComponent['action'];
|
action: UserGuideDropdownAction['action'];
|
||||||
}) => {
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('UserGuideDropdown', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'UserGuideDropdown',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
text: resolveReactElement(text),
|
text: resolveReactElement(text),
|
||||||
action,
|
action,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -554,22 +561,18 @@ export default class PluginRegistry {
|
|||||||
action,
|
action,
|
||||||
filter,
|
filter,
|
||||||
}: {
|
}: {
|
||||||
text: ReactResolvable;
|
text: PostDropdownMenuAction['text'];
|
||||||
action: PluginComponent['action'];
|
action: PostDropdownMenuAction['action'];
|
||||||
filter: PluginComponent['filter'];
|
filter: PostDropdownMenuAction['filter'];
|
||||||
}) => {
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('PostDropdownMenu', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'PostDropdownMenu',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
text: resolveReactElement(text),
|
text: resolveReactElement(text),
|
||||||
action,
|
action,
|
||||||
filter,
|
filter,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -593,23 +596,20 @@ export default class PluginRegistry {
|
|||||||
filter,
|
filter,
|
||||||
}: {
|
}: {
|
||||||
text: ReactResolvable;
|
text: ReactResolvable;
|
||||||
action: PluginComponent['action'];
|
action: PostDropdownMenuAction['action'];
|
||||||
filter: PluginComponent['filter'];
|
filter: PostDropdownMenuAction['filter'];
|
||||||
}) => {
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
const registerMenuItem = (
|
const registerMenuItem = (
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
id: string,
|
id: string,
|
||||||
parentMenuId: string | null,
|
parentMenuId: string | undefined,
|
||||||
innerText: ReactResolvable,
|
innerText: ReactResolvable,
|
||||||
innerAction: PluginComponent['action'],
|
innerAction: PostDropdownMenuAction['action'],
|
||||||
innerFilter: PluginComponent['filter'],
|
innerFilter: PostDropdownMenuAction['filter'],
|
||||||
) => {
|
) => {
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('PostDropdownMenu', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'PostDropdownMenu',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
parentMenuId,
|
parentMenuId,
|
||||||
pluginId,
|
pluginId,
|
||||||
@@ -617,13 +617,12 @@ export default class PluginRegistry {
|
|||||||
subMenu: [],
|
subMenu: [],
|
||||||
action: innerAction,
|
action: innerAction,
|
||||||
filter: innerFilter,
|
filter: innerFilter,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type TInnerParams = [
|
type TInnerParams = [
|
||||||
innerText: ReactResolvable,
|
innerText: ReactResolvable,
|
||||||
innerAction: PluginComponent['action'],
|
innerAction: PostDropdownMenuAction['action'],
|
||||||
innerFilter: PluginComponent['filter'],
|
innerFilter: PostDropdownMenuAction['filter'],
|
||||||
];
|
];
|
||||||
|
|
||||||
return function registerSubMenuItem(...args: TInnerParams) {
|
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.
|
// Register a component at the bottom of the post dropdown menu.
|
||||||
@@ -660,21 +659,17 @@ export default class PluginRegistry {
|
|||||||
text,
|
text,
|
||||||
}: {
|
}: {
|
||||||
icon: ReactResolvable;
|
icon: ReactResolvable;
|
||||||
action: PluginComponent['action'];
|
action: FileUploadMethodAction['action'];
|
||||||
text: ReactResolvable;
|
text: ReactResolvable;
|
||||||
}) => {
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('FileUploadMethod', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'FileUploadMethod',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
text,
|
text,
|
||||||
action,
|
action,
|
||||||
icon,
|
icon,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -687,17 +682,15 @@ export default class PluginRegistry {
|
|||||||
// - message - An error message to display, leave blank or null to display no message
|
// - 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
|
// - files - Modified array of files to upload, set to null to reject all files
|
||||||
// Returns a unique identifier.
|
// Returns a unique identifier.
|
||||||
registerFilesWillUploadHook = reArg(['hook'], ({hook}) => {
|
registerFilesWillUploadHook = reArg(['hook'], ({hook}: {
|
||||||
|
hook: FilesWillUploadHook['hook'];
|
||||||
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('FilesWillUploadHook', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'FilesWillUploadHook',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
hook,
|
hook,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
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
|
// If the hook function is asynchronous, the message will not be sent to the server
|
||||||
// until the hook returns.
|
// until the hook returns.
|
||||||
registerMessageWillBePostedHook = reArg(['hook'], ({hook}) => {
|
registerMessageWillBePostedHook = reArg(['hook'], ({hook}: {
|
||||||
|
hook: MessageWillBePostedHook['hook'];
|
||||||
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('MessageWillBePosted', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'MessageWillBePosted',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
hook,
|
hook,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
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
|
// If the hook function is asynchronous, the command will not be sent to the server
|
||||||
// until the hook returns.
|
// until the hook returns.
|
||||||
registerSlashCommandWillBePostedHook = reArg(['hook'], ({hook}) => {
|
registerSlashCommandWillBePostedHook = reArg(['hook'], ({hook}: {
|
||||||
|
hook: SlashCommandWillBePostedHook['hook'];
|
||||||
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('SlashCommandWillBePosted', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'SlashCommandWillBePosted',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
hook,
|
hook,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -828,17 +817,15 @@ export default class PluginRegistry {
|
|||||||
// already modified by other hooks) as arguments. This function must return a string
|
// already modified by other hooks) as arguments. This function must return a string
|
||||||
// message that will be formatted.
|
// message that will be formatted.
|
||||||
// Returns a unique identifier.
|
// Returns a unique identifier.
|
||||||
registerMessageWillFormatHook = reArg(['hook'], ({hook}) => {
|
registerMessageWillFormatHook = reArg(['hook'], ({hook}: {
|
||||||
|
hook: MessageWillFormatHook['hook'];
|
||||||
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('MessageWillFormat', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'MessageWillFormat',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
hook,
|
hook,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
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.
|
// - component - A react component to display instead of original preview. Receives fileInfo and post as props.
|
||||||
// Returns a unique identifier.
|
// 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.
|
// 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();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('FilePreview', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'FilePreview',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
override,
|
override,
|
||||||
component,
|
component,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -908,7 +894,7 @@ export default class PluginRegistry {
|
|||||||
options: {showTitle} = {showTitle: false},
|
options: {showTitle} = {showTitle: false},
|
||||||
}: {
|
}: {
|
||||||
key: string;
|
key: string;
|
||||||
component: PluginComponent['component'];
|
component: AdminConsolePluginComponent['component'];
|
||||||
options?: {showTitle: boolean};
|
options?: {showTitle: boolean};
|
||||||
}) => {
|
}) => {
|
||||||
store.dispatch(registerAdminConsoleCustomSetting(this.id, key, component, {showTitle}));
|
store.dispatch(registerAdminConsoleCustomSetting(this.id, key, component, {showTitle}));
|
||||||
@@ -926,7 +912,7 @@ export default class PluginRegistry {
|
|||||||
component,
|
component,
|
||||||
}: {
|
}: {
|
||||||
key: string;
|
key: string;
|
||||||
component: PluginComponent['component'];
|
component: AdminConsolePluginCustomSection['component'];
|
||||||
}) => {
|
}) => {
|
||||||
store.dispatch(registerAdminConsoleCustomSection(this.id, key, 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.
|
// - showRHSPlugin: the action to dispatch that will open the RHS.
|
||||||
// - hideRHSPlugin: the action to dispatch that will close the RHS
|
// - hideRHSPlugin: the action to dispatch that will close the RHS
|
||||||
// - toggleRHSPlugin: the action to dispatch that will toggle 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();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('RightHandSidebarComponent', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'RightHandSidebarComponent',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
component,
|
component,
|
||||||
title: resolveReactElement(title),
|
title: resolveReactElement(title),
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {id, showRHSPlugin: showRHSPlugin(id), hideRHSPlugin: hideRHSPlugin(id), toggleRHSPlugin: toggleRHSPlugin(id)};
|
return {id, showRHSPlugin: showRHSPlugin(id), hideRHSPlugin: hideRHSPlugin(id), toggleRHSPlugin: toggleRHSPlugin(id)};
|
||||||
@@ -977,15 +968,11 @@ export default class PluginRegistry {
|
|||||||
let fixedRoute = standardizeRoute(route);
|
let fixedRoute = standardizeRoute(route);
|
||||||
fixedRoute = this.id + '/' + fixedRoute;
|
fixedRoute = this.id + '/' + fixedRoute;
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('NeedsTeamComponent', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'NeedsTeamComponent',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
component,
|
component,
|
||||||
route: fixedRoute,
|
route: fixedRoute,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -1007,21 +994,17 @@ export default class PluginRegistry {
|
|||||||
component,
|
component,
|
||||||
}: {
|
}: {
|
||||||
route: string;
|
route: string;
|
||||||
component: PluginComponent['component'];
|
component: CustomRouteComponent['component'];
|
||||||
}) => {
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
let fixedRoute = standardizeRoute(route);
|
let fixedRoute = standardizeRoute(route);
|
||||||
fixedRoute = this.id + '/' + fixedRoute;
|
fixedRoute = this.id + '/' + fixedRoute;
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('CustomRouteComponent', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'CustomRouteComponent',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
component,
|
component,
|
||||||
route: fixedRoute,
|
route: fixedRoute,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -1061,10 +1044,7 @@ export default class PluginRegistry {
|
|||||||
}: Omit<ProductComponent, 'id' | 'pluginId'>) => {
|
}: Omit<ProductComponent, 'id' | 'pluginId'>) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('Product', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'Product',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
switcherIcon,
|
switcherIcon,
|
||||||
@@ -1078,7 +1058,6 @@ export default class PluginRegistry {
|
|||||||
showAppBar,
|
showAppBar,
|
||||||
wrapped,
|
wrapped,
|
||||||
publicComponent,
|
publicComponent,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
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
|
// If the hook function is asynchronous, the message will not be sent to the server
|
||||||
// until the hook returns.
|
// until the hook returns.
|
||||||
registerMessageWillBeUpdatedHook = reArg(['hook'], ({hook}) => {
|
registerMessageWillBeUpdatedHook = reArg(['hook'], ({hook}: {
|
||||||
|
hook: MessageWillBeUpdatedHook['hook'];
|
||||||
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('MessageWillBeUpdated', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'MessageWillBeUpdated',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
hook,
|
hook,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
@@ -1162,26 +1139,23 @@ export default class PluginRegistry {
|
|||||||
rhsComponent,
|
rhsComponent,
|
||||||
rhsTitle,
|
rhsTitle,
|
||||||
}: {
|
}: {
|
||||||
iconUrl: string;
|
iconUrl: AppBarAction['iconUrl'];
|
||||||
tooltipText: ReactResolvable;
|
tooltipText: ReactResolvable;
|
||||||
supportedProductIds: ProductScope;
|
supportedProductIds: AppBarAction['supportedProductIds'];
|
||||||
} & ({
|
} & ({
|
||||||
action: PluginComponent['action'];
|
action: AppBarChannelAction;
|
||||||
rhsComponent?: never;
|
rhsComponent?: never;
|
||||||
rhsTitle?: never;
|
rhsTitle?: never;
|
||||||
} | {
|
} | {
|
||||||
action?: never;
|
action?: never;
|
||||||
rhsComponent: PluginComponent;
|
rhsComponent: RightHandSidebarComponent['component'];
|
||||||
rhsTitle: ReactResolvable;
|
rhsTitle: ReactResolvable;
|
||||||
})) => {
|
})) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
const registeredRhsComponent = rhsComponent && this.registerRightHandSidebarComponent({title: rhsTitle, component: rhsComponent});
|
const registeredRhsComponent = rhsComponent && this.registerRightHandSidebarComponent({title: rhsTitle, component: rhsComponent});
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('AppBar', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'AppBar',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
iconUrl,
|
iconUrl,
|
||||||
@@ -1191,8 +1165,7 @@ export default class PluginRegistry {
|
|||||||
action: () => store.dispatch(registeredRhsComponent.toggleRHSPlugin),
|
action: () => store.dispatch(registeredRhsComponent.toggleRHSPlugin),
|
||||||
rhsComponentId: registeredRhsComponent.id,
|
rhsComponentId: registeredRhsComponent.id,
|
||||||
} : {
|
} : {
|
||||||
action,
|
action: action!,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1247,17 +1220,15 @@ export default class PluginRegistry {
|
|||||||
// completed. The resulting args will be used as the arguments for the `notifyMe` function.
|
// completed. The resulting args will be used as the arguments for the `notifyMe` function.
|
||||||
//
|
//
|
||||||
// Returns a unique identifier.
|
// Returns a unique identifier.
|
||||||
registerDesktopNotificationHook = reArg(['hook'], ({hook}) => {
|
registerDesktopNotificationHook = reArg(['hook'], ({hook}: {
|
||||||
|
hook: DesktopNotificationHook['hook'];
|
||||||
|
}) => {
|
||||||
const id = generateId();
|
const id = generateId();
|
||||||
|
|
||||||
store.dispatch({
|
dispatchPluginComponentWithData('DesktopNotificationHooks', {
|
||||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
|
|
||||||
name: 'DesktopNotificationHooks',
|
|
||||||
data: {
|
|
||||||
id,
|
id,
|
||||||
pluginId: this.id,
|
pluginId: this.id,
|
||||||
hook,
|
hook,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import {connect} from 'react-redux';
|
|||||||
import {getPluggableId} from 'selectors/rhs';
|
import {getPluggableId} from 'selectors/rhs';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
import type {PluginComponent} from 'types/store/plugins';
|
|
||||||
|
|
||||||
import RHSPlugin from './rhs_plugin';
|
import RHSPlugin from './rhs_plugin';
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
const rhsPlugins: PluginComponent[] = state.plugins.components.RightHandSidebarComponent;
|
const rhsPlugins = state.plugins.components.RightHandSidebarComponent;
|
||||||
const pluggableId = getPluggableId(state);
|
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 : '';
|
const pluginTitle = pluginComponent ? pluginComponent.title : '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -345,6 +345,11 @@ exports[`plugins/MainMenuActions should match snapshot in mobile view with some
|
|||||||
</Memo(MenuGroup)>
|
</Memo(MenuGroup)>
|
||||||
<Memo(MenuGroup)>
|
<Memo(MenuGroup)>
|
||||||
<MenuItemAction
|
<MenuItemAction
|
||||||
|
icon={
|
||||||
|
<i
|
||||||
|
className="fa fa-anchor"
|
||||||
|
/>
|
||||||
|
}
|
||||||
id="someplugin_pluginmenuitem"
|
id="someplugin_pluginmenuitem"
|
||||||
key="someplugin_pluginmenuitem"
|
key="someplugin_pluginmenuitem"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
|||||||
@@ -3,16 +3,6 @@
|
|||||||
exports[`plugins/PostMessageView should match snapshot with extended post type 1`] = `
|
exports[`plugins/PostMessageView should match snapshot with extended post type 1`] = `
|
||||||
<PostMessageView
|
<PostMessageView
|
||||||
currentRelativeTeamUrl="team_url"
|
currentRelativeTeamUrl="team_url"
|
||||||
currentUser={
|
|
||||||
Object {
|
|
||||||
"username": "username",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emojis={
|
|
||||||
Object {
|
|
||||||
"name": "smile",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enableFormatting={true}
|
enableFormatting={true}
|
||||||
isRHS={false}
|
isRHS={false}
|
||||||
options={Object {}}
|
options={Object {}}
|
||||||
@@ -20,6 +10,9 @@ exports[`plugins/PostMessageView should match snapshot with extended post type 1
|
|||||||
Object {
|
Object {
|
||||||
"testtype": Object {
|
"testtype": Object {
|
||||||
"component": [Function],
|
"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",
|
"type": "testtype",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
team={
|
|
||||||
Object {
|
|
||||||
"name": "team_name",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
theme={
|
theme={
|
||||||
Object {
|
Object {
|
||||||
"awayIndicator": "#ffbc1f",
|
"awayIndicator": "#ffbc1f",
|
||||||
@@ -145,7 +133,7 @@ exports[`plugins/PostMessageView should match snapshot with no extended post typ
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Connect(Pluggable)
|
<Pluggable
|
||||||
onHeightChange={[Function]}
|
onHeightChange={[Function]}
|
||||||
pluggableName="PostMessageAttachment"
|
pluggableName="PostMessageAttachment"
|
||||||
postId="post_id"
|
postId="post_id"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {ComponentProps} from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
@@ -12,49 +13,37 @@ import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
|||||||
describe('plugins/MainMenuActions', () => {
|
describe('plugins/MainMenuActions', () => {
|
||||||
const pluginAction = jest.fn();
|
const pluginAction = jest.fn();
|
||||||
|
|
||||||
const requiredProps = {
|
const requiredProps: ComponentProps<typeof MainMenu> = {
|
||||||
teamId: 'someteamid',
|
teamId: 'someteamid',
|
||||||
teamType: '',
|
|
||||||
teamDisplayName: 'some name',
|
|
||||||
teamName: 'somename',
|
teamName: 'somename',
|
||||||
currentUser: {id: 'someuserid', roles: 'system_user'} as UserProfile,
|
currentUser: {id: 'someuserid', roles: 'system_user'} as UserProfile,
|
||||||
enableCommands: true,
|
enableCommands: true,
|
||||||
enableCustomEmoji: true,
|
|
||||||
enableIncomingWebhooks: true,
|
enableIncomingWebhooks: true,
|
||||||
enableOutgoingWebhooks: true,
|
enableOutgoingWebhooks: true,
|
||||||
enableOAuthServiceProvider: true,
|
enableOAuthServiceProvider: true,
|
||||||
canManageSystemBots: true,
|
canManageSystemBots: true,
|
||||||
enableUserCreation: true,
|
pluginMenuItems: [{
|
||||||
enableEmailInvitations: false,
|
id: 'someplugin',
|
||||||
enablePluginMarketplace: true,
|
pluginId: 'test',
|
||||||
showDropdown: true,
|
text: 'some plugin text',
|
||||||
onToggleDropdown: () => {}, //eslint-disable-line no-empty-function
|
action: pluginAction,
|
||||||
pluginMenuItems: [{id: 'someplugin', pluginId: 'test', text: 'some plugin text', action: pluginAction}],
|
mobileIcon: <i className='fa fa-anchor'/>,
|
||||||
canCreateOrDeleteCustomEmoji: true,
|
}],
|
||||||
canManageIntegrations: true,
|
canManageIntegrations: true,
|
||||||
moreTeamsToJoin: true,
|
moreTeamsToJoin: true,
|
||||||
guestAccessEnabled: true,
|
guestAccessEnabled: true,
|
||||||
teamIsGroupConstrained: true,
|
teamIsGroupConstrained: true,
|
||||||
teamUrl: '/team',
|
|
||||||
location: {
|
|
||||||
pathname: '/team',
|
|
||||||
},
|
|
||||||
actions: {
|
actions: {
|
||||||
openModal: jest.fn(),
|
openModal: jest.fn(),
|
||||||
showMentions: jest.fn(),
|
showMentions: jest.fn(),
|
||||||
showFlaggedPosts: jest.fn(),
|
showFlaggedPosts: jest.fn(),
|
||||||
closeRightHandSide: jest.fn(),
|
closeRightHandSide: jest.fn(),
|
||||||
closeRhsMenu: jest.fn(),
|
closeRhsMenu: jest.fn(),
|
||||||
getCloudLimits: jest.fn(),
|
|
||||||
},
|
},
|
||||||
isCloud: false,
|
isCloud: false,
|
||||||
isStarterFree: false,
|
isStarterFree: false,
|
||||||
subscription: {},
|
|
||||||
userIsAdmin: true,
|
|
||||||
isFirstAdmin: false,
|
|
||||||
canInviteTeamMember: false,
|
canInviteTeamMember: false,
|
||||||
isFreeTrial: false,
|
isFreeTrial: false,
|
||||||
teamsLimitReached: false,
|
|
||||||
usageDeltaTeams: -1,
|
usageDeltaTeams: -1,
|
||||||
mobile: false,
|
mobile: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {shallow, mount} from 'enzyme';
|
import {shallow, mount} from 'enzyme';
|
||||||
|
import type {ComponentProps} from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {Preferences} from 'mattermost-redux/constants';
|
import {Preferences} from 'mattermost-redux/constants';
|
||||||
@@ -14,16 +15,17 @@ const PostTypePlugin = () => (
|
|||||||
|
|
||||||
describe('plugins/PostMessageView', () => {
|
describe('plugins/PostMessageView', () => {
|
||||||
const post = {type: 'testtype', message: 'this is some text', id: 'post_id'} as any;
|
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,
|
post,
|
||||||
pluginPostTypes,
|
pluginPostTypes: {
|
||||||
currentUser: {username: 'username'},
|
testtype: {
|
||||||
team: {name: 'team_name'},
|
id: 'some id',
|
||||||
emojis: {name: 'smile'},
|
pluginId: 'some plugin id',
|
||||||
|
component: PostTypePlugin,
|
||||||
|
type: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
theme: Preferences.THEMES.denim,
|
theme: Preferences.THEMES.denim,
|
||||||
enableFormatting: true,
|
enableFormatting: true,
|
||||||
currentRelativeTeamUrl: 'team_url',
|
currentRelativeTeamUrl: 'team_url',
|
||||||
|
|||||||
@@ -16,13 +16,12 @@ import {extractPluginConfiguration} from 'utils/plugins/plugin_setting_extractio
|
|||||||
import type {MMAction} from 'types/store';
|
import type {MMAction} from 'types/store';
|
||||||
import type {
|
import type {
|
||||||
PluginsState,
|
PluginsState,
|
||||||
PluginComponent,
|
|
||||||
AdminConsolePluginComponent,
|
AdminConsolePluginComponent,
|
||||||
AdminConsolePluginCustomSection,
|
AdminConsolePluginCustomSection,
|
||||||
Menu,
|
PostDropdownMenuAction,
|
||||||
} from 'types/store/plugins';
|
} from 'types/store/plugins';
|
||||||
|
|
||||||
function hasMenuId(menu: Menu|PluginComponent, menuId: string) {
|
function hasMenuId(menu: PostDropdownMenuAction, menuId: string) {
|
||||||
if (!menu.subMenu) {
|
if (!menu.subMenu) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -39,20 +38,20 @@ function hasMenuId(menu: Menu|PluginComponent, menuId: string) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMenu(rootMenu: Menu|PluginComponent, data: Menu): Menu|PluginComponent {
|
function buildMenu(rootMenu: PostDropdownMenuAction, data: PostDropdownMenuAction): PostDropdownMenuAction {
|
||||||
// Recursively build the full menu tree.
|
// 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) {
|
if (rootMenu.id === data.parentMenuId) {
|
||||||
subMenu?.push(data);
|
subMenu?.push(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rootMenu,
|
...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) {
|
if (a.pluginId < b.pluginId) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -106,7 +105,7 @@ function removePluginComponents(state: PluginsState['components'], action: AnyAc
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextState = {...state};
|
const nextState = {...state};
|
||||||
const types = Object.keys(nextState);
|
const types = Object.keys(nextState) as Array<keyof PluginsState['components']>;
|
||||||
let modified = false;
|
let modified = false;
|
||||||
for (let i = 0; i < types.length; i++) {
|
for (let i = 0; i < types.length; i++) {
|
||||||
const componentType = types[i];
|
const componentType = types[i];
|
||||||
@@ -115,7 +114,7 @@ function removePluginComponents(state: PluginsState['components'], action: AnyAc
|
|||||||
if (componentList[j].pluginId === action.data.id) {
|
if (componentList[j].pluginId === action.data.id) {
|
||||||
const nextArray = [...nextState[componentType]];
|
const nextArray = [...nextState[componentType]];
|
||||||
nextArray.splice(j, 1);
|
nextArray.splice(j, 1);
|
||||||
nextState[componentType] = nextArray;
|
nextState[componentType] = nextArray as any;
|
||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +129,7 @@ function removePluginComponents(state: PluginsState['components'], action: AnyAc
|
|||||||
|
|
||||||
function removePluginComponent(state: PluginsState['components'], action: AnyAction) {
|
function removePluginComponent(state: PluginsState['components'], action: AnyAction) {
|
||||||
let newState = state;
|
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++) {
|
for (let i = 0; i < types.length; i++) {
|
||||||
const componentType = types[i];
|
const componentType = types[i];
|
||||||
const componentList = state[componentType] || [];
|
const componentList = state[componentType] || [];
|
||||||
@@ -195,14 +194,33 @@ const initialComponents: PluginsState['components'] = {
|
|||||||
NewMessagesSeparatorAction: [],
|
NewMessagesSeparatorAction: [],
|
||||||
Product: [],
|
Product: [],
|
||||||
RightHandSidebarComponent: [],
|
RightHandSidebarComponent: [],
|
||||||
UserGuideDropdownItem: [],
|
|
||||||
FilesWillUploadHook: [],
|
FilesWillUploadHook: [],
|
||||||
NeedsTeamComponent: [],
|
NeedsTeamComponent: [],
|
||||||
CreateBoardFromTemplate: [],
|
CreateBoardFromTemplate: [],
|
||||||
DesktopNotificationHooks: [],
|
DesktopNotificationHooks: [],
|
||||||
|
BottomTeamSidebar: [],
|
||||||
|
ChannelHeader: [],
|
||||||
|
ChannelIntroButton: [],
|
||||||
|
CustomRouteComponent: [],
|
||||||
|
FilesDropdown: [],
|
||||||
|
FileUploadMethod: [],
|
||||||
|
LeftSidebarHeader: [],
|
||||||
|
MessageWillFormat: [],
|
||||||
|
PopoverUserActions: [],
|
||||||
|
PopoverUserAttributes: [],
|
||||||
|
PostDropdownMenuItem: [],
|
||||||
|
PostMessageAttachment: [],
|
||||||
|
PostWillRenderEmbedComponent: [],
|
||||||
|
Root: [],
|
||||||
|
SearchButtons: [],
|
||||||
|
SearchHints: [],
|
||||||
|
SearchSuggestions: [],
|
||||||
|
UserGuideDropdown: [],
|
||||||
|
ChannelToast: [],
|
||||||
|
Global: [],
|
||||||
|
SidebarChannelLinkLabel: [],
|
||||||
MessageWillBePosted: [],
|
MessageWillBePosted: [],
|
||||||
MessageWillBeUpdated: [],
|
MessageWillBeUpdated: [],
|
||||||
MessageWillFormat: [],
|
|
||||||
SlashCommandWillBePosted: [],
|
SlashCommandWillBePosted: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,13 +228,14 @@ function components(state: PluginsState['components'] = initialComponents, actio
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.RECEIVED_PLUGIN_COMPONENT: {
|
case ActionTypes.RECEIVED_PLUGIN_COMPONENT: {
|
||||||
if (action.name && action.data) {
|
if (action.name && action.data) {
|
||||||
|
const pluggableType = action.name as keyof PluginsState['components'];
|
||||||
const nextState = {...state};
|
const nextState = {...state};
|
||||||
const currentArray = nextState[action.name] || [];
|
const currentArray = nextState[pluggableType] || [];
|
||||||
const nextArray = [...currentArray];
|
const nextArray = [...currentArray];
|
||||||
let actionData = action.data;
|
let actionData = action.data;
|
||||||
if (action.name === 'PostDropdownMenu' && actionData.parentMenuId) {
|
if (action.name === 'PostDropdownMenu' && actionData.parentMenuId) {
|
||||||
// Remove the menu from nextArray to rebuild it later.
|
// 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.
|
// Request is for an unknown menuId, return original state.
|
||||||
if (!menu[0]) {
|
if (!menu[0]) {
|
||||||
@@ -226,7 +245,7 @@ function components(state: PluginsState['components'] = initialComponents, actio
|
|||||||
}
|
}
|
||||||
nextArray.push(actionData);
|
nextArray.push(actionData);
|
||||||
nextArray.sort(sortComponents);
|
nextArray.sort(sortComponents);
|
||||||
nextState[action.name] = nextArray;
|
nextState[pluggableType] = nextArray as any;
|
||||||
return nextState;
|
return nextState;
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import type {AppBinding} from '@mattermost/types/apps';
|
|
||||||
|
|
||||||
import {Preferences} from 'mattermost-redux/constants';
|
import {Preferences} from 'mattermost-redux/constants';
|
||||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||||
import {appBarEnabled, getAppBarAppBindings} from 'mattermost-redux/selectors/entities/apps';
|
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 {createShallowSelector} from 'mattermost-redux/utils/helpers';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
import type {FileDropdownPluginComponent, PluginComponent} from 'types/store/plugins';
|
|
||||||
|
|
||||||
export const getPluginUserSettings = createSelector(
|
export const getPluginUserSettings = createSelector(
|
||||||
'getPluginUserSettings',
|
'getPluginUserSettings',
|
||||||
@@ -25,7 +22,7 @@ export const getFilesDropdownPluginMenuItems = createSelector(
|
|||||||
'getFilesDropdownPluginMenuItems',
|
'getFilesDropdownPluginMenuItems',
|
||||||
(state: GlobalState) => state.plugins.components.FilesDropdown,
|
(state: GlobalState) => state.plugins.components.FilesDropdown,
|
||||||
(components) => {
|
(components) => {
|
||||||
return (components || []) as unknown as FileDropdownPluginComponent[];
|
return (components || []);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -44,7 +41,7 @@ export const getChannelHeaderPluginComponents = createSelector(
|
|||||||
(state: GlobalState) => state.plugins.components.AppBar,
|
(state: GlobalState) => state.plugins.components.AppBar,
|
||||||
(enabled, channelHeaderComponents = [], appBarComponents = []) => {
|
(enabled, channelHeaderComponents = [], appBarComponents = []) => {
|
||||||
if (!enabled || !appBarComponents.length) {
|
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
|
// Remove channel header icons for plugins that have also registered an app bar component
|
||||||
@@ -99,7 +96,7 @@ export const shouldShowAppBar = createSelector(
|
|||||||
getAppBarAppBindings,
|
getAppBarAppBindings,
|
||||||
getAppBarPluginComponents,
|
getAppBarPluginComponents,
|
||||||
getChannelHeaderPluginComponents,
|
getChannelHeaderPluginComponents,
|
||||||
(enabled: boolean, bindings: AppBinding[], appBarComponents: PluginComponent[], channelHeaderComponents) => {
|
(enabled, bindings, appBarComponents, channelHeaderComponents) => {
|
||||||
return enabled && Boolean(bindings.length || appBarComponents.length || channelHeaderComponents.length);
|
return enabled && Boolean(bindings.length || appBarComponents.length || channelHeaderComponents.length);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,17 +2,23 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
import type {RouteComponentProps} from 'react-router-dom';
|
||||||
|
|
||||||
import type {WebSocketClient} from '@mattermost/client';
|
import type {WebSocketClient} from '@mattermost/client';
|
||||||
import type {IconGlyphTypes} from '@mattermost/compass-icons/IconGlyphs';
|
import type {IconGlyphTypes} from '@mattermost/compass-icons/IconGlyphs';
|
||||||
import type {PluginAnalyticsRow} from '@mattermost/types/admin';
|
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 {FileInfo} from '@mattermost/types/files';
|
||||||
|
import type {CommandArgs} from '@mattermost/types/integrations';
|
||||||
import type {ClientPluginManifest} from '@mattermost/types/plugins';
|
import type {ClientPluginManifest} from '@mattermost/types/plugins';
|
||||||
import type {Post, PostEmbed} from '@mattermost/types/posts';
|
import type {Post, PostEmbed} from '@mattermost/types/posts';
|
||||||
import type {ProductScope} from '@mattermost/types/products';
|
import type {ProductScope} from '@mattermost/types/products';
|
||||||
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
import type {IDMappedObjects} from '@mattermost/types/utilities';
|
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 {NewPostMessageProps} from 'actions/new_post';
|
||||||
|
|
||||||
import type {PluginConfiguration} from 'types/plugins/user_settings';
|
import type {PluginConfiguration} from 'types/plugins/user_settings';
|
||||||
@@ -24,30 +30,48 @@ export type PluginsState = {
|
|||||||
plugins: IDMappedObjects<ClientPluginManifest>;
|
plugins: IDMappedObjects<ClientPluginManifest>;
|
||||||
|
|
||||||
components: {
|
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[];
|
Product: ProductComponent[];
|
||||||
CallButton: PluginComponent[];
|
PostDropdownMenuItem: PostDropdownMenuItemComponent[];
|
||||||
PostDropdownMenu: PluginComponent[];
|
PostAction: PostActionComponent[];
|
||||||
PostAction: PluginComponent[];
|
PostEditorAction: PostEditorActionComponent[];
|
||||||
PostEditorAction: PluginComponent[];
|
CodeBlockAction: CodeBlockActionComponent[];
|
||||||
CodeBlockAction: PluginComponent[];
|
NewMessagesSeparatorAction: NewMessagesSeparatorActionComponent[];
|
||||||
NewMessagesSeparatorAction: PluginComponent[];
|
FilePreview: FilePreviewComponent[];
|
||||||
FilePreview: PluginComponent[];
|
LinkTooltip: LinkTooltipComponent[];
|
||||||
MainMenu: PluginComponent[];
|
RightHandSidebarComponent: RightHandSidebarComponent[];
|
||||||
LinkTooltip: PluginComponent[];
|
|
||||||
RightHandSidebarComponent: PluginComponent[];
|
|
||||||
ChannelHeaderButton: PluginComponent[];
|
|
||||||
MobileChannelHeaderButton: PluginComponent[];
|
|
||||||
AppBar: AppBarComponent[];
|
|
||||||
UserGuideDropdownItem: PluginComponent[];
|
|
||||||
FilesWillUploadHook: PluginComponent[];
|
|
||||||
NeedsTeamComponent: NeedsTeamComponent[];
|
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[];
|
DesktopNotificationHooks: DesktopNotificationHook[];
|
||||||
SlashCommandWillBePosted: SlashCommandWillBePostedHook[];
|
|
||||||
MessageWillBePosted: MessageWillBePostedHook[];
|
|
||||||
MessageWillBeUpdated: MessageWillBeUpdatedHook[];
|
|
||||||
MessageWillFormat: MessageWillFormatHook[];
|
MessageWillFormat: MessageWillFormatHook[];
|
||||||
|
MessageWillBePosted: MessageWillBePostedHook[];
|
||||||
|
SlashCommandWillBePosted: SlashCommandWillBePostedHook[];
|
||||||
|
MessageWillBeUpdated: MessageWillBeUpdatedHook[];
|
||||||
};
|
};
|
||||||
|
|
||||||
postTypes: {
|
postTypes: {
|
||||||
@@ -86,106 +110,128 @@ export type PluginsState = {
|
|||||||
export type Menu = {
|
export type Menu = {
|
||||||
id: string;
|
id: string;
|
||||||
parentMenuId?: string;
|
parentMenuId?: string;
|
||||||
text?: React.ReactElement | string;
|
text?: PluggableText;
|
||||||
selectedValueText?: string;
|
selectedValueText?: string;
|
||||||
subMenu?: Menu[];
|
subMenu?: Menu[];
|
||||||
filter?: (id?: string) => boolean;
|
filter?: (id: string) => boolean;
|
||||||
action?: (...args: any) => void;
|
action?: (...args: any) => void;
|
||||||
icon?: React.ReactElement;
|
icon?: React.ReactNode;
|
||||||
direction?: 'left' | 'right';
|
direction?: 'left' | 'right';
|
||||||
isHeader?: boolean;
|
isHeader?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginComponent = {
|
type PluginComponent = {
|
||||||
id: string;
|
id: string;
|
||||||
pluginId: 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 & {
|
type BasePluggableProps = {
|
||||||
rhsComponentId?: string;
|
webSocketClient: WebSocketClient;
|
||||||
|
theme: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NeedsTeamComponent = PluginComponent & {
|
export type PluggableText = string | React.ReactNode;
|
||||||
route: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FilesWillUploadHook = {
|
export type AppBarChannelAction = (channel: Channel, member: ChannelMembership) => void;
|
||||||
hook: (files: File[], uploadFiles: (files: File[]) => void) => { message?: string; files?: File[] };
|
export type AppBarAction = PluginComponent & {
|
||||||
}
|
iconUrl: string;
|
||||||
|
supportedProductIds: ProductScope;
|
||||||
|
tooltipText: PluggableText;
|
||||||
|
} & ({
|
||||||
|
action: AppBarChannelAction;
|
||||||
|
} | {
|
||||||
|
rhsComponentId: string;
|
||||||
|
action: () => {data: boolean};
|
||||||
|
});
|
||||||
|
|
||||||
export type FilePreviewComponent = {
|
export type FilesDropdownAction = PluginComponent & {
|
||||||
id: string;
|
text: PluggableText;
|
||||||
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;
|
|
||||||
match: (fileInfo: FileInfo) => boolean;
|
match: (fileInfo: FileInfo) => boolean;
|
||||||
action: (fileInfo: FileInfo) => void;
|
action: (fileInfo: FileInfo) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PostPluginComponent = {
|
export type PostDropdownMenuAction = PluginComponent & {
|
||||||
id: string;
|
parentMenuId?: string;
|
||||||
pluginId: string;
|
subMenu?: PostDropdownMenuAction[];
|
||||||
type: string;
|
text: PluggableText;
|
||||||
component: React.ElementType;
|
action: (postId: string) => void;
|
||||||
|
filter: (postId: string) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminConsolePluginComponent = {
|
export type ChannelHeaderAction = PluginComponent & {
|
||||||
pluginId: string;
|
text: PluggableText;
|
||||||
key: string;
|
action: (channelId: string) => void;
|
||||||
component: React.Component;
|
shouldRender: (state: GlobalState) => boolean;
|
||||||
options: {
|
|
||||||
showTitle: boolean;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminConsolePluginCustomSection = {
|
export type ChannelHeaderButtonAction = PluginComponent & {
|
||||||
pluginId: string;
|
icon: React.ReactNode;
|
||||||
key: string;
|
dropdownText: PluggableText;
|
||||||
component: React.Component;
|
tooltipText: PluggableText;
|
||||||
|
action: (channel: Channel, member?: ChannelMembership) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PostWillRenderEmbedPluginComponent = {
|
export type FileUploadMethodAction = PluginComponent & {
|
||||||
id: string;
|
text: PluggableText;
|
||||||
pluginId: string;
|
action: (checkPluginHooksAndUploadFiles: ((files: FileList | File[]) => void)) => void;
|
||||||
component: React.ComponentType<{ embed: PostEmbed; webSocketClient?: WebSocketClient }>;
|
icon: React.ReactNode;
|
||||||
match: (arg: PostEmbed) => boolean;
|
};
|
||||||
toggleable: boolean;
|
|
||||||
|
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[] };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
type ProductBaseProps = {theme: Theme};
|
||||||
* The main uuid of the product.
|
export type ProductSubComponentNames = 'mainComponent' | 'publicComponent' | 'headerCentreComponent' | 'headerRightComponent';
|
||||||
*/
|
export type ProductComponent = PluginComponent & {
|
||||||
id: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The plain identifier of the source plugin
|
|
||||||
*/
|
|
||||||
pluginId: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A compass-icon glyph to display as the icon in the product switcher
|
* 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.
|
* 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.
|
* 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
|
* A component to fill the generic area in the center of
|
||||||
* the global header when your route is active.
|
* 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
|
* A component to fill the generic area in the right of
|
||||||
* the global header when your route is active.
|
* 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.
|
* A flag to display or hide the team sidebar in products.
|
||||||
@@ -248,45 +296,191 @@ export type ProductComponent = {
|
|||||||
wrapped: boolean;
|
wrapped: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DesktopNotificationArgs = {
|
export type NeedsTeamComponent = PluginComponent & {
|
||||||
title: string;
|
route: string;
|
||||||
body: string;
|
component: React.ComponentType<BasePluggableProps>;
|
||||||
silent: boolean;
|
}
|
||||||
soundName: string;
|
|
||||||
url: string;
|
|
||||||
notify: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DesktopNotificationHook = PluginComponent & {
|
export type FilePreviewComponent = PluginComponent & {
|
||||||
hook: (post: Post, msgProps: NewPostMessageProps, channel: Channel, teamId: string, args: DesktopNotificationArgs) => Promise<{
|
override: (fileInfo: FileInfo, post?: Post) => boolean;
|
||||||
error?: string;
|
component: React.ComponentType<{
|
||||||
args?: DesktopNotificationArgs;
|
fileInfo: FileInfo;
|
||||||
|
post?: Post;
|
||||||
|
onModalDismissed: () => void;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlashCommandWillBePostedArgs = {
|
export type PostWillRenderEmbedComponent = PluginComponent & {
|
||||||
channel_id: string;
|
component: React.ComponentType<{
|
||||||
team_id?: string;
|
embed: PostEmbed;
|
||||||
root_id?: string;
|
webSocketClient?: WebSocketClient;
|
||||||
|
}>;
|
||||||
|
match: (arg: PostEmbed) => boolean;
|
||||||
|
toggleable: boolean;
|
||||||
}
|
}
|
||||||
export type SlashCommandWillBePostedHook = PluginComponent & {
|
|
||||||
hook: (message: string, args: SlashCommandWillBePostedArgs) => Promise<(
|
export type PostDropdownMenuItemComponent = PluginComponent & {
|
||||||
{error: {message: string}} | {message: string; args: SlashCommandWillBePostedArgs} | Record<string, never>
|
text: PluggableText;
|
||||||
)>;
|
component: React.ComponentType<BasePluggableProps & {postId: string}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageWillBePostedHook = PluginComponent & {
|
export type RightHandSidebarComponent = PluginComponent & {
|
||||||
hook: (post: Post) => Promise<(
|
title: PluggableText;
|
||||||
{error: {message: string}} | {post: Post}
|
component: React.ComponentType<BasePluggableProps>;
|
||||||
)>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageWillBeUpdatedHook = PluginComponent & {
|
export type SearchHintsComponent = PluginComponent & {
|
||||||
hook: (newPost: Partial<Post>, oldPost: Post) => Promise<(
|
component: React.ComponentType<{
|
||||||
{error: {message: string}} | {post: Partial<Post>}
|
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 & {
|
export type MessageWillFormatHook = PluginComponent & {
|
||||||
hook: (post: Post, message: string) => string;
|
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,
|
showTeamSidebar: false,
|
||||||
showAppBar: false,
|
showAppBar: false,
|
||||||
wrapped: true,
|
wrapped: true,
|
||||||
publicComponent: null,
|
publicComponent: () => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user