Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
28
webapp/channels/src/plugins/actions.js
Обычный файл
28
webapp/channels/src/plugins/actions.js
Обычный файл
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ActionTypes} from 'utils/constants';
|
||||
import {hideRHSPlugin as hideRHSPluginAction} from 'actions/views/rhs';
|
||||
import {getPluggableId} from 'selectors/rhs';
|
||||
|
||||
export const removeWebappPlugin = (manifest) => {
|
||||
return (dispatch) => {
|
||||
dispatch(hideRHSPlugin(manifest.id));
|
||||
dispatch({type: ActionTypes.REMOVED_WEBAPP_PLUGIN, data: manifest});
|
||||
};
|
||||
};
|
||||
|
||||
// hideRHSPlugin closes the RHS if currently showing this plugin.
|
||||
const hideRHSPlugin = (manifestId) => {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const rhsPlugins = state.plugins.components.RightHandSidebarComponent || [];
|
||||
const pluggableId = getPluggableId(state);
|
||||
const pluginComponent = rhsPlugins.find((element) => element.id === pluggableId && element.pluginId === manifestId);
|
||||
|
||||
// Hide RHS if its showing this plugin
|
||||
if (pluginComponent) {
|
||||
dispatch(hideRHSPluginAction(pluggableId));
|
||||
}
|
||||
};
|
||||
};
|
||||
133
webapp/channels/src/plugins/call_button/call_button.scss
Обычный файл
133
webapp/channels/src/plugins/call_button/call_button.scss
Обычный файл
@@ -0,0 +1,133 @@
|
||||
button {
|
||||
&.call-button {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 12px;
|
||||
border-radius: 4px;
|
||||
color: rgb(var(--button-bg-rgb));
|
||||
fill: currentColor;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
background: rgba(var(--button-bg-rgb), 0.16);
|
||||
}
|
||||
|
||||
i,
|
||||
span {
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
i::before,
|
||||
span::before {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
span.call-button-label {
|
||||
padding: 1px 0;
|
||||
margin: 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 9px;
|
||||
}
|
||||
|
||||
&.dropdown {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.32);
|
||||
cursor: not-allowed;
|
||||
fill: currentColor;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.call-button-dropdown {
|
||||
.Menu .MenuItem > & {
|
||||
display: flex;
|
||||
padding: 6px 20px 6px 16px;
|
||||
margin: 0;
|
||||
color: rgb(var(--button-bg-rgb));
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-self: flex-start;
|
||||
margin-right: 12px;
|
||||
margin-left: 4px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
i {
|
||||
align-self: flex-start;
|
||||
margin-right: 12px;
|
||||
margin-left: 4px;
|
||||
color: inherit;
|
||||
font-size: 16px;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.call-button-dropdown-sublabel {
|
||||
margin-top: 4px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.32);
|
||||
fill: currentColor;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
i {
|
||||
color: inherit;
|
||||
font-size: 16px;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.call-button-dropdown-sublabel {
|
||||
color: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
webapp/channels/src/plugins/call_button/call_button.tsx
Обычный файл
116
webapp/channels/src/plugins/call_button/call_button.tsx
Обычный файл
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {CSSProperties, useState, useEffect, useRef} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import PhoneOutlineIcon from '@mattermost/compass-icons/components/phone-outline';
|
||||
import ChevronDownIcon from '@mattermost/compass-icons/components/chevron-down';
|
||||
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
import {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||
import {PluginComponent} from 'types/store/plugins';
|
||||
|
||||
import './call_button.scss';
|
||||
|
||||
type Props = {
|
||||
currentChannel: Channel;
|
||||
channelMember?: ChannelMembership;
|
||||
pluginCallComponents: PluginComponent[];
|
||||
sidebarOpen: boolean;
|
||||
}
|
||||
|
||||
export default function CallButton({pluginCallComponents, currentChannel, channelMember, sidebarOpen}: Props) {
|
||||
const [active, setActive] = useState(false);
|
||||
const [clickEnabled, setClickEnabled] = useState(true);
|
||||
const prevSidebarOpen = useRef(sidebarOpen);
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
if (prevSidebarOpen.current && !sidebarOpen) {
|
||||
setClickEnabled(false);
|
||||
setTimeout(() => {
|
||||
setClickEnabled(true);
|
||||
}, Constants.CHANNEL_HEADER_BUTTON_DISABLE_TIMEOUT);
|
||||
}
|
||||
prevSidebarOpen.current = sidebarOpen;
|
||||
}, [sidebarOpen]);
|
||||
|
||||
if (pluginCallComponents.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = {
|
||||
container: {
|
||||
marginTop: 16,
|
||||
height: 32,
|
||||
} as CSSProperties,
|
||||
};
|
||||
|
||||
if (pluginCallComponents.length === 1) {
|
||||
const item = pluginCallComponents[0];
|
||||
const clickHandler = () => item.action?.(currentChannel, channelMember);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style.container}
|
||||
className='flex-child'
|
||||
onClick={clickEnabled ? clickHandler : undefined}
|
||||
onTouchEnd={clickEnabled ? clickHandler : undefined}
|
||||
>
|
||||
{item.button}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const items = pluginCallComponents.map((item) => {
|
||||
return (
|
||||
<li
|
||||
className='MenuItem'
|
||||
key={item.id}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
item.action?.(currentChannel, channelMember);
|
||||
}}
|
||||
>
|
||||
{item.dropdownButton}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style.container}
|
||||
className='flex-child'
|
||||
>
|
||||
<MenuWrapper onToggle={(toggle: boolean) => setActive(toggle)}>
|
||||
<button className={classNames('style--none call-button dropdown', {active})}>
|
||||
<PhoneOutlineIcon
|
||||
color='inherit'
|
||||
aria-label={formatMessage({id: 'generic_icons.call', defaultMessage: 'Call icon'}).toLowerCase()}
|
||||
/>
|
||||
<span className='call-button-label'>{'Call'}</span>
|
||||
<ChevronDownIcon
|
||||
color='inherit'
|
||||
aria-label={formatMessage({id: 'generic_icons.dropdown', defaultMessage: 'Dropdown icon'}).toLowerCase()}
|
||||
/>
|
||||
</button>
|
||||
<Menu
|
||||
id='callOptions'
|
||||
ariaLabel={formatMessage({id: 'call_button.menuAriaLabel', defaultMessage: 'Call type selector'})}
|
||||
customStyles={{
|
||||
top: 'auto',
|
||||
left: 'auto',
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
{items}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
webapp/channels/src/plugins/call_button/index.ts
Обычный файл
21
webapp/channels/src/plugins/call_button/index.ts
Обычный файл
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {GlobalState} from 'types/store/index';
|
||||
|
||||
import CallButton from './call_button';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
currentChannel: getCurrentChannel(state),
|
||||
pluginCallComponents: state.plugins.components.CallButton,
|
||||
channelMember: getMyCurrentChannelMembership(state),
|
||||
sidebarOpen: state.views.rhs.isSidebarOpen,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CallButton);
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import ChannelHeaderPlug from 'plugins/channel_header_plug/channel_header_plug';
|
||||
import {mountWithIntl} from '../../tests/helpers/intl-test-helper';
|
||||
import {PluginComponent} from 'types/store/plugins';
|
||||
|
||||
describe('plugins/ChannelHeaderPlug', () => {
|
||||
const testPlug: PluginComponent = {
|
||||
id: 'someid',
|
||||
pluginId: 'pluginid',
|
||||
icon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn,
|
||||
dropdownText: 'some dropdown text',
|
||||
tooltipText: 'some tooltip text',
|
||||
} as PluginComponent;
|
||||
|
||||
test('should match snapshot with no extended component', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={{} as Channel}
|
||||
channelMember={{} as ChannelMembership}
|
||||
theme={{} as Theme}
|
||||
sidebarOpen={false}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
appBindings={[]}
|
||||
appsEnabled={false}
|
||||
shouldShowAppBar={false}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with one extended component', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ChannelHeaderPlug
|
||||
components={[testPlug]}
|
||||
channel={{} as Channel}
|
||||
channelMember={{} as ChannelMembership}
|
||||
theme={{} as Theme}
|
||||
sidebarOpen={false}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
appBindings={[]}
|
||||
appsEnabled={false}
|
||||
shouldShowAppBar={false}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with six extended components', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ChannelHeaderPlug
|
||||
components={[
|
||||
testPlug,
|
||||
{...testPlug, id: 'someid2'},
|
||||
{...testPlug, id: 'someid3'},
|
||||
{...testPlug, id: 'someid4'},
|
||||
{...testPlug, id: 'someid5'},
|
||||
{...testPlug, id: 'someid6'},
|
||||
{...testPlug, id: 'someid7'},
|
||||
{...testPlug, id: 'someid8'},
|
||||
{...testPlug, id: 'someid9'},
|
||||
{...testPlug, id: 'someid10'},
|
||||
{...testPlug, id: 'someid11'},
|
||||
{...testPlug, id: 'someid12'},
|
||||
{...testPlug, id: 'someid13'},
|
||||
{...testPlug, id: 'someid14'},
|
||||
{...testPlug, id: 'someid15'},
|
||||
]}
|
||||
channel={{} as Channel}
|
||||
channelMember={{} as ChannelMembership}
|
||||
theme={{} as Theme}
|
||||
sidebarOpen={false}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
appBindings={[]}
|
||||
appsEnabled={false}
|
||||
shouldShowAppBar={false}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when the App Bar is visible', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ChannelHeaderPlug
|
||||
components={[
|
||||
testPlug,
|
||||
{...testPlug, id: 'someid2'},
|
||||
{...testPlug, id: 'someid3'},
|
||||
{...testPlug, id: 'someid4'},
|
||||
]}
|
||||
channel={{} as Channel}
|
||||
channelMember={{} as ChannelMembership}
|
||||
theme={{} as Theme}
|
||||
sidebarOpen={false}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
appBindings={[]}
|
||||
appsEnabled={false}
|
||||
shouldShowAppBar={true}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,361 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
|
||||
import React from 'react';
|
||||
import {Dropdown, Tooltip} from 'react-bootstrap';
|
||||
import {RootCloseWrapper} from 'react-overlays';
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||
|
||||
import {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {AppBinding} from '@mattermost/types/apps';
|
||||
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
|
||||
|
||||
import {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForChannel} from 'types/apps';
|
||||
|
||||
import HeaderIconWrapper from 'components/channel_header/components/header_icon_wrapper';
|
||||
import PluginChannelHeaderIcon from 'components/widgets/icons/plugin_channel_header_icon';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
|
||||
import {PluginComponent} from 'types/store/plugins';
|
||||
|
||||
import {createCallContext} from 'utils/apps';
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
type CustomMenuProps = {
|
||||
open?: boolean;
|
||||
children?: React.ReactNode;
|
||||
onClose: () => void;
|
||||
rootCloseEvent?: 'click' | 'mousedown';
|
||||
bsRole: string;
|
||||
}
|
||||
|
||||
class CustomMenu extends React.PureComponent<CustomMenuProps> {
|
||||
handleRootClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
open,
|
||||
rootCloseEvent,
|
||||
children,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<RootCloseWrapper
|
||||
disabled={!open}
|
||||
onRootClose={this.handleRootClose}
|
||||
event={rootCloseEvent}
|
||||
>
|
||||
<ul
|
||||
role='menu'
|
||||
className='dropdown-menu channel-header_plugin-dropdown'
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
</RootCloseWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type CustomToggleProps = {
|
||||
children?: React.ReactNode;
|
||||
dropdownOpen?: boolean;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
bsRole: string;
|
||||
}
|
||||
|
||||
class CustomToggle extends React.PureComponent<CustomToggleProps> {
|
||||
handleClick = (e: React.MouseEvent) => {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children} = this.props;
|
||||
|
||||
let activeClass = '';
|
||||
if (this.props.dropdownOpen) {
|
||||
activeClass = ' channel-header__icon--active';
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
id='pluginChannelHeaderButtonDropdown'
|
||||
className={'channel-header__icon channel-header__icon--wide ' + activeClass}
|
||||
type='button'
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type ChannelHeaderPlugProps = {
|
||||
intl: IntlShape;
|
||||
components: PluginComponent[];
|
||||
appBindings?: AppBinding[];
|
||||
appsEnabled: boolean;
|
||||
channel: Channel;
|
||||
channelMember?: ChannelMembership;
|
||||
theme: Theme;
|
||||
sidebarOpen: boolean;
|
||||
shouldShowAppBar: boolean;
|
||||
actions: {
|
||||
handleBindingClick: HandleBindingClick;
|
||||
postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel;
|
||||
openAppsModal: OpenAppsModal;
|
||||
};
|
||||
}
|
||||
|
||||
type ChannelHeaderPlugState = {
|
||||
dropdownOpen: boolean;
|
||||
}
|
||||
|
||||
class ChannelHeaderPlug extends React.PureComponent<ChannelHeaderPlugProps, ChannelHeaderPlugState> {
|
||||
public static defaultProps: Partial<ChannelHeaderPlugProps> = {
|
||||
components: [],
|
||||
appBindings: [],
|
||||
}
|
||||
|
||||
private disableButtonsClosingRHS = false;
|
||||
|
||||
constructor(props: ChannelHeaderPlugProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dropdownOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ChannelHeaderPlugProps) {
|
||||
if (prevProps.sidebarOpen && !this.props.sidebarOpen) {
|
||||
this.disableButtonsClosingRHS = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.disableButtonsClosingRHS = false;
|
||||
}, Constants.CHANNEL_HEADER_BUTTON_DISABLE_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
toggleDropdown = (dropdownOpen: boolean) => {
|
||||
this.setState({dropdownOpen});
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.toggleDropdown(false);
|
||||
}
|
||||
|
||||
fireAction = (action: (channel: Channel, channelMember?: ChannelMembership) => void) => {
|
||||
if (this.disableButtonsClosingRHS) {
|
||||
return;
|
||||
}
|
||||
|
||||
action(this.props.channel, this.props.channelMember);
|
||||
}
|
||||
|
||||
fireActionAndClose = (action: (channel: Channel, channelMember?: ChannelMembership) => void) => {
|
||||
action(this.props.channel, this.props.channelMember);
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
createComponentButton = (plug: PluginComponent) => {
|
||||
return (
|
||||
<HeaderIconWrapper
|
||||
key={'channelHeaderButton' + plug.id}
|
||||
buttonClass='channel-header__icon'
|
||||
iconComponent={plug.icon!}
|
||||
onClick={() => this.fireAction(plug.action!)}
|
||||
buttonId={plug.id}
|
||||
tooltipKey={'plugin'}
|
||||
tooltipText={plug.tooltipText ? plug.tooltipText : plug.dropdownText}
|
||||
pluginId={plug.pluginId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onBindingClick = async (binding: AppBinding) => {
|
||||
if (this.disableButtonsClosingRHS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {channel, intl} = this.props;
|
||||
|
||||
const context = createCallContext(
|
||||
binding.app_id,
|
||||
binding.location,
|
||||
this.props.channel.id,
|
||||
this.props.channel.team_id,
|
||||
);
|
||||
|
||||
const res = await this.props.actions.handleBindingClick(binding, context, intl);
|
||||
|
||||
if (res.error) {
|
||||
const errorResponse = res.error;
|
||||
const errorMessage = errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error occurred.',
|
||||
});
|
||||
this.props.actions.postEphemeralCallResponseForChannel(errorResponse, errorMessage, channel.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const callResp = res.data!;
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.text) {
|
||||
this.props.actions.postEphemeralCallResponseForChannel(callResp, callResp.text, channel.id);
|
||||
}
|
||||
break;
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
break;
|
||||
case AppCallResponseTypes.FORM:
|
||||
if (callResp.form) {
|
||||
this.props.actions.openAppsModal(callResp.form, context);
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
const errorMessage = this.props.intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {
|
||||
type: callResp.type,
|
||||
});
|
||||
this.props.actions.postEphemeralCallResponseForChannel(callResp, errorMessage, channel.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createAppBindingButton = (binding: AppBinding) => {
|
||||
return (
|
||||
<HeaderIconWrapper
|
||||
key={`channelHeaderButton_${binding.app_id}_${binding.location}`}
|
||||
buttonClass='channel-header__icon style--none'
|
||||
iconComponent={(
|
||||
<img
|
||||
src={binding.icon}
|
||||
width='24'
|
||||
height='24'
|
||||
/>
|
||||
)}
|
||||
onClick={() => this.onBindingClick(binding)}
|
||||
buttonId={`${binding.app_id}_${binding.location}`}
|
||||
tooltipKey={'plugin'}
|
||||
tooltipText={binding.label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
createDropdown = (plugs: PluginComponent[], appBindings: AppBinding[]) => {
|
||||
const componentItems = plugs.filter((plug) => plug.action).map((plug) => {
|
||||
return (
|
||||
<li
|
||||
key={'channelHeaderPlug' + plug.id}
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
className='d-flex align-items-center'
|
||||
onClick={() => this.fireActionAndClose(plug.action!)}
|
||||
>
|
||||
<span className='d-flex align-items-center overflow--ellipsis'>{plug.icon}</span>
|
||||
<span>{plug.dropdownText}</span>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
let items = componentItems;
|
||||
if (this.props.appsEnabled) {
|
||||
items = componentItems.concat(appBindings.map((binding) => {
|
||||
return (
|
||||
<li
|
||||
key={'channelHeaderPlug' + binding.app_id + binding.location}
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
className='d-flex align-items-center'
|
||||
onClick={() => this.fireActionAndClose(() => this.onBindingClick(binding))}
|
||||
>
|
||||
<span className='d-flex align-items-center overflow--ellipsis icon'>{(<img src={binding.icon}/>)}</span>
|
||||
<span>{binding.label}</span>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex-child'>
|
||||
<Dropdown
|
||||
id='channelHeaderPlugDropdown'
|
||||
onToggle={this.toggleDropdown}
|
||||
open={this.state.dropdownOpen}
|
||||
>
|
||||
<CustomToggle
|
||||
bsRole='toggle'
|
||||
dropdownOpen={this.state.dropdownOpen}
|
||||
>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='bottom'
|
||||
overlay={this.state.dropdownOpen ? <></> : (
|
||||
<Tooltip id='removeIcon'>
|
||||
<div aria-hidden={true}>
|
||||
<FormattedMessage
|
||||
id='generic_icons.plugins'
|
||||
defaultMessage='Plugins'
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<React.Fragment>
|
||||
<PluginChannelHeaderIcon
|
||||
id='pluginChannelHeaderIcon'
|
||||
className='icon icon--standard icon__pluginChannelHeader'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<span
|
||||
id='pluginCount'
|
||||
className='icon__text'
|
||||
>
|
||||
{items.length}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
</OverlayTrigger>
|
||||
</CustomToggle>
|
||||
<CustomMenu
|
||||
bsRole='menu'
|
||||
open={this.state.dropdownOpen}
|
||||
onClose={this.onClose}
|
||||
>
|
||||
{items}
|
||||
</CustomMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const components = this.props.components || [];
|
||||
const appBindings = this.props.appsEnabled ? this.props.appBindings || [] : [];
|
||||
if (this.props.shouldShowAppBar || (components.length === 0 && appBindings.length === 0)) {
|
||||
return null;
|
||||
} else if ((components.length + appBindings.length) <= 15) {
|
||||
let componentButtons = components.filter((plug) => plug.icon && plug.action).map(this.createComponentButton);
|
||||
if (this.props.appsEnabled) {
|
||||
componentButtons = componentButtons.concat(appBindings.map(this.createAppBindingButton));
|
||||
}
|
||||
return componentButtons;
|
||||
}
|
||||
|
||||
return this.createDropdown(components, appBindings);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(ChannelHeaderPlug);
|
||||
/* eslint-enable react/no-multi-comp */
|
||||
49
webapp/channels/src/plugins/channel_header_plug/index.ts
Обычный файл
49
webapp/channels/src/plugins/channel_header_plug/index.ts
Обычный файл
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {appBarEnabled, appsEnabled, getChannelHeaderAppBindings} from 'mattermost-redux/selectors/entities/apps';
|
||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForChannel} from 'types/apps';
|
||||
|
||||
import {handleBindingClick, openAppsModal, postEphemeralCallResponseForChannel} from 'actions/apps';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {getChannelHeaderPluginComponents, shouldShowAppBar} from 'selectors/plugins';
|
||||
|
||||
import ChannelHeaderPlug from './channel_header_plug';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const apps = appsEnabled(state);
|
||||
return {
|
||||
components: getChannelHeaderPluginComponents(state),
|
||||
appBindings: getChannelHeaderAppBindings(state),
|
||||
appsEnabled: apps,
|
||||
appBarEnabled: appBarEnabled(state),
|
||||
theme: getTheme(state),
|
||||
sidebarOpen: state.views.rhs.isSidebarOpen,
|
||||
shouldShowAppBar: shouldShowAppBar(state),
|
||||
};
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
handleBindingClick: HandleBindingClick;
|
||||
postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel;
|
||||
openAppsModal: OpenAppsModal;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<any>, Actions>({
|
||||
handleBindingClick,
|
||||
postEphemeralCallResponseForChannel,
|
||||
openAppsModal,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelHeaderPlug);
|
||||
36
webapp/channels/src/plugins/docs.json
Обычный файл
36
webapp/channels/src/plugins/docs.json
Обычный файл
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Root": {
|
||||
"desc": "A component at the root of any logged-in pages. Can be used for creating modals and pop-ups that display over the whole app. Does not override any existing component.",
|
||||
"props": {}
|
||||
},
|
||||
"PostTypePlugin": {
|
||||
"desc": "The component displaying the message body of posts. For adding new components based on post type. Use in your `postTypeComponents` argument to `registerComponents` in your plugin's intialization function.",
|
||||
"props": {
|
||||
"post": {
|
||||
"type": { "name": "object" },
|
||||
"required": true,
|
||||
"desc": "Post to be displayed"
|
||||
},
|
||||
"compactDisplay": {
|
||||
"type": { "name": "bool" },
|
||||
"required": false,
|
||||
"defaultValue": false,
|
||||
"desc": "Set to true if UI is in compact display mode"
|
||||
},
|
||||
"isRHS": {
|
||||
"type": { "name": "bool" },
|
||||
"required": true,
|
||||
"defaultValue": false,
|
||||
"desc": "Set to true if the post is in the right-hand sidebar (thread, search results, etc.)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ChannelHeaderButton": {
|
||||
"desc": "A component on the right side of the channel header, beside buttons such as the pinned posts button.",
|
||||
"props": {}
|
||||
},
|
||||
"MobileChannelHeaderButton": {
|
||||
"desc": "Same as ChannelHeaderButton, except shown in mobile view when the screen is less than 768 pixels wide.",
|
||||
"props": {}
|
||||
}
|
||||
}
|
||||
85
webapp/channels/src/plugins/export.js
Обычный файл
85
webapp/channels/src/plugins/export.js
Обычный файл
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {closeRightHandSide, selectPostById} from 'actions/views/rhs';
|
||||
import {getSelectedPostId, getIsRhsOpen} from 'selectors/rhs';
|
||||
|
||||
import BotTag from 'components/widgets/tag/bot_tag';
|
||||
|
||||
import messageHtmlToComponent from 'utils/message_html_to_component';
|
||||
import {formatText} from 'utils/text_formatting';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {useWebSocket, useWebSocketClient, WebSocketContext} from 'utils/use_websocket';
|
||||
import {imageURLForUser} from 'utils/utils';
|
||||
|
||||
import ChannelInviteModal from 'components/channel_invite_modal';
|
||||
import ChannelMembersModal from 'components/channel_members_modal';
|
||||
import PurchaseModal from 'components/purchase_modal';
|
||||
import {useNotifyAdmin} from 'components/notify_admin_cta/notify_admin_cta';
|
||||
import Timestamp from 'components/timestamp';
|
||||
import Avatar from 'components/widgets/users/avatar';
|
||||
|
||||
import {openPricingModal} from '../components/global_header/right_controls/plan_upgrade_button';
|
||||
|
||||
import Textbox from './textbox';
|
||||
|
||||
// The following import has intentional side effects. Do not remove without research.
|
||||
import {openInteractiveDialog} from './interactive_dialog';
|
||||
|
||||
// Common libraries exposed on window for plugins to use as Webpack externals.
|
||||
window.React = require('react');
|
||||
window.ReactDOM = require('react-dom');
|
||||
window.ReactIntl = require('react-intl');
|
||||
window.Redux = require('redux');
|
||||
window.ReactRedux = require('react-redux');
|
||||
window.ReactBootstrap = require('react-bootstrap');
|
||||
window.ReactRouterDom = require('react-router-dom');
|
||||
window.PropTypes = require('prop-types');
|
||||
window.Luxon = require('luxon');
|
||||
window.StyledComponents = require('styled-components');
|
||||
|
||||
// Functions exposed on window for plugins to use.
|
||||
window.PostUtils = {formatText, messageHtmlToComponent};
|
||||
window.openInteractiveDialog = openInteractiveDialog;
|
||||
window.useNotifyAdmin = useNotifyAdmin;
|
||||
window.WebappUtils = {
|
||||
modals: {openModal, ModalIdentifiers},
|
||||
};
|
||||
Object.defineProperty(window.WebappUtils, 'browserHistory', {
|
||||
get: () => getHistory(),
|
||||
});
|
||||
|
||||
// This need to be a function because `openPricingModal`
|
||||
// is initialized when `UpgradeCloudButton` is loaded.
|
||||
// So if we export `openPricingModal` directly, it will be locked
|
||||
// to the initial value of undefined.
|
||||
window.openPricingModal = () => openPricingModal;
|
||||
|
||||
// Components exposed on window FOR INTERNAL PLUGIN USE ONLY. These components may have breaking changes in the future
|
||||
// outside of major releases. They will be replaced by common components once that project is more mature and able to
|
||||
// guarantee better compatibility.
|
||||
window.Components = {
|
||||
Textbox,
|
||||
PurchaseModal,
|
||||
Timestamp,
|
||||
ChannelInviteModal,
|
||||
ChannelMembersModal,
|
||||
Avatar,
|
||||
imageURLForUser,
|
||||
BotBadge: BotTag,
|
||||
};
|
||||
|
||||
// This is a prototype of the Product API for use by internal plugins only while we transition to the proper architecture
|
||||
// for them using module federation.
|
||||
window.ProductApi = {
|
||||
useWebSocket,
|
||||
useWebSocketClient,
|
||||
WebSocketProvider: WebSocketContext,
|
||||
closeRhs: closeRightHandSide,
|
||||
selectRhsPost: selectPostById,
|
||||
getRhsSelectedPostId: getSelectedPostId,
|
||||
getIsRhsOpen,
|
||||
};
|
||||
243
webapp/channels/src/plugins/index.js
Обычный файл
243
webapp/channels/src/plugins/index.js
Обычный файл
@@ -0,0 +1,243 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import regeneratorRuntime from 'regenerator-runtime';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import store from 'stores/redux_store.jsx';
|
||||
import {ActionTypes} from 'utils/constants';
|
||||
import {getSiteURL} from 'utils/url';
|
||||
import PluginRegistry from 'plugins/registry';
|
||||
import {unregisterAllPluginWebSocketEvents, unregisterPluginReconnectHandler} from 'actions/websocket_actions.jsx';
|
||||
import {unregisterPluginTranslationsSource} from 'actions/views/root';
|
||||
import {unregisterAdminConsolePlugin} from 'actions/admin_actions';
|
||||
import {trackPluginInitialization} from 'actions/telemetry_actions';
|
||||
|
||||
import {removeWebappPlugin} from './actions';
|
||||
|
||||
// Including the fullscreen modal css to make it available to the plugins
|
||||
// (without lazy loading). This should be removed in the future whenever we
|
||||
// have all plugins migrated to common components that can be reused there.
|
||||
import 'components/widgets/modals/full_screen_modal.scss';
|
||||
|
||||
// Plugins may have been compiled with the regenerator runtime. Ensure this remains available
|
||||
// as a global export even though the webapp does not depend on same.
|
||||
window.regeneratorRuntime = regeneratorRuntime;
|
||||
|
||||
// plugins records all active web app plugins by id.
|
||||
window.plugins = {};
|
||||
|
||||
// registerPlugin, on the global window object, should be invoked by a plugin's web app bundle as
|
||||
// it is loaded.
|
||||
//
|
||||
// During the beta, plugins manipulated the global window.plugins data structure directly. This
|
||||
// remains possible, but is officially deprecated and may be removed in a future release.
|
||||
function registerPlugin(id, plugin) {
|
||||
const oldPlugin = window.plugins[id];
|
||||
if (oldPlugin && oldPlugin.uninitialize) {
|
||||
oldPlugin.uninitialize();
|
||||
}
|
||||
|
||||
window.plugins[id] = plugin;
|
||||
}
|
||||
window.registerPlugin = registerPlugin;
|
||||
|
||||
function arePluginsEnabled(state) {
|
||||
if (getConfig(state).PluginsEnabled !== 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
isPerformanceDebuggingEnabled(state) &&
|
||||
getBool(state, Preferences.CATEGORY_PERFORMANCE_DEBUGGING, Preferences.NAME_DISABLE_CLIENT_PLUGINS)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// initializePlugins queries the server for all enabled plugins and loads each in turn.
|
||||
export async function initializePlugins() {
|
||||
if (!arePluginsEnabled(store.getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {data, error} = await getPlugins()(store.dispatch);
|
||||
if (error) {
|
||||
console.error(error); //eslint-disable-line no-console
|
||||
return;
|
||||
}
|
||||
|
||||
if (data == null || data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(data.map((m) => {
|
||||
return loadPlugin(m).catch((loadErr) => {
|
||||
console.error(loadErr.message); //eslint-disable-line no-console
|
||||
});
|
||||
}));
|
||||
|
||||
trackPluginInitialization(data);
|
||||
}
|
||||
|
||||
// getPlugins queries the server for all enabled plugins
|
||||
export function getPlugins() {
|
||||
return async (dispatch) => {
|
||||
let plugins;
|
||||
try {
|
||||
plugins = await Client4.getWebappPlugins();
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
dispatch({type: ActionTypes.RECEIVED_WEBAPP_PLUGINS, data: plugins});
|
||||
|
||||
return {data: plugins};
|
||||
};
|
||||
}
|
||||
|
||||
// loadedPlugins tracks which plugins have been added as script tags to the page
|
||||
const loadedPlugins = {};
|
||||
|
||||
// describePlugin takes a manifest and spits out a string suitable for console.log messages.
|
||||
const describePlugin = (manifest) => (
|
||||
'plugin ' + manifest.id + ', version ' + manifest.version
|
||||
);
|
||||
|
||||
// loadPlugin fetches the web app bundle described by the given manifest, waits for the bundle to
|
||||
// load, and then ensures the plugin has been initialized.
|
||||
export function loadPlugin(manifest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!arePluginsEnabled(store.getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't load it again if previously loaded
|
||||
const oldManifest = loadedPlugins[manifest.id];
|
||||
if (oldManifest && oldManifest.webapp.bundle_path === manifest.webapp.bundle_path) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldManifest) {
|
||||
// upgrading, perform cleanup
|
||||
store.dispatch(removeWebappPlugin(manifest));
|
||||
}
|
||||
|
||||
function onLoad() {
|
||||
initializePlugin(manifest);
|
||||
console.log('Loaded ' + describePlugin(manifest)); //eslint-disable-line no-console
|
||||
resolve();
|
||||
}
|
||||
|
||||
function onError() {
|
||||
reject(new Error('Unable to load bundle for ' + describePlugin(manifest)));
|
||||
}
|
||||
|
||||
// Backwards compatibility for old plugins
|
||||
let bundlePath = manifest.webapp.bundle_path;
|
||||
if (bundlePath.includes('/static/') && !bundlePath.includes('/static/plugins/')) {
|
||||
bundlePath = bundlePath.replace('/static/', '/static/plugins/');
|
||||
}
|
||||
|
||||
console.log('Loading ' + describePlugin(manifest)); //eslint-disable-line no-console
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.id = 'plugin_' + manifest.id;
|
||||
script.type = 'text/javascript';
|
||||
script.src = getSiteURL() + bundlePath;
|
||||
script.onload = onLoad;
|
||||
script.onerror = onError;
|
||||
|
||||
document.getElementsByTagName('head')[0].appendChild(script);
|
||||
loadedPlugins[manifest.id] = manifest;
|
||||
});
|
||||
}
|
||||
|
||||
// initializePlugin creates a registry specific to the plugin and invokes any initialize function
|
||||
// on the registered plugin class.
|
||||
function initializePlugin(manifest) {
|
||||
// Initialize the plugin
|
||||
const plugin = window.plugins[manifest.id];
|
||||
const registry = new PluginRegistry(manifest.id);
|
||||
if (plugin && plugin.initialize) {
|
||||
plugin.initialize(registry, store);
|
||||
}
|
||||
}
|
||||
|
||||
// removePlugin triggers any uninitialize callback on the registered plugin, unregisters any
|
||||
// event handlers, and removes the plugin script from the DOM entirely. The plugin is responsible
|
||||
// for removing any of its registered components.
|
||||
export function removePlugin(manifest) {
|
||||
if (!loadedPlugins[manifest.id]) {
|
||||
return;
|
||||
}
|
||||
console.log('Removing ' + describePlugin(manifest)); //eslint-disable-line no-console
|
||||
|
||||
delete loadedPlugins[manifest.id];
|
||||
|
||||
store.dispatch(removeWebappPlugin(manifest));
|
||||
|
||||
const plugin = window.plugins[manifest.id];
|
||||
if (plugin && plugin.uninitialize) {
|
||||
plugin.uninitialize();
|
||||
|
||||
// Support the deprecated deinitialize callback from the plugins beta.
|
||||
} else if (plugin && plugin.deinitialize) {
|
||||
plugin.deinitialize();
|
||||
}
|
||||
unregisterAllPluginWebSocketEvents(manifest.id);
|
||||
unregisterPluginReconnectHandler(manifest.id);
|
||||
store.dispatch(unregisterAdminConsolePlugin(manifest.id));
|
||||
unregisterPluginTranslationsSource(manifest.id);
|
||||
const script = document.getElementById('plugin_' + manifest.id);
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
script.parentNode.removeChild(script);
|
||||
console.log('Removed ' + describePlugin(manifest)); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
// loadPluginsIfNecessary synchronizes the current state of loaded plugins with that of the server,
|
||||
// loading any newly added plugins and unloading any removed ones.
|
||||
export async function loadPluginsIfNecessary() {
|
||||
if (!arePluginsEnabled(store.getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldManifests = store.getState().plugins.plugins;
|
||||
|
||||
const {error} = await getPlugins()(store.dispatch);
|
||||
if (error) {
|
||||
console.error(error); //eslint-disable-line no-console
|
||||
return;
|
||||
}
|
||||
|
||||
const newManifests = store.getState().plugins.plugins;
|
||||
|
||||
// Get new plugins and update existing plugins if version changed
|
||||
Object.values(newManifests).forEach((newManifest) => {
|
||||
const oldManifest = oldManifests[newManifest.id];
|
||||
if (!oldManifest || oldManifest.version !== newManifest.version) {
|
||||
loadPlugin(newManifest).catch((loadErr) => {
|
||||
console.error(loadErr.message); //eslint-disable-line no-console
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Remove old plugins
|
||||
Object.keys(oldManifests).forEach((id) => {
|
||||
if (!newManifests.hasOwnProperty(id)) {
|
||||
const oldManifest = oldManifests[id];
|
||||
removePlugin(oldManifest);
|
||||
}
|
||||
});
|
||||
}
|
||||
45
webapp/channels/src/plugins/interactive_dialog.js
Обычный файл
45
webapp/channels/src/plugins/interactive_dialog.js
Обычный файл
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
IntegrationTypes,
|
||||
} from 'mattermost-redux/action_types';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import InteractiveDialog from 'components/interactive_dialog';
|
||||
import store from '../stores/redux_store';
|
||||
|
||||
export function openInteractiveDialog(dialog) {
|
||||
store.dispatch({type: IntegrationTypes.RECEIVED_DIALOG, data: dialog});
|
||||
|
||||
store.dispatch(openModal({modalId: ModalIdentifiers.INTERACTIVE_DIALOG, dialogType: InteractiveDialog}));
|
||||
}
|
||||
|
||||
// This code is problematic for a couple of different reasons:
|
||||
// * it monitors the store to modify the store: this is perhaps better handled by a saga
|
||||
// * it makes importing this file impure by triggering a side-effect which may not be obvious
|
||||
// * it's not really located in the "right place": dialogs are applicable to non-plugins too
|
||||
// * it's nigh impossible to test as written
|
||||
//
|
||||
// It's worth fixing all of this, but I think this requires some refactoring.
|
||||
let previousTriggerId = '';
|
||||
store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
const currentTriggerId = state.entities.integrations.dialogTriggerId;
|
||||
|
||||
if (currentTriggerId === previousTriggerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
previousTriggerId = currentTriggerId;
|
||||
|
||||
const dialog = state.entities.integrations.dialog || {};
|
||||
if (dialog.trigger_id !== currentTriggerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch(openModal({modalId: ModalIdentifiers.INTERACTIVE_DIALOG, dialogType: InteractiveDialog}));
|
||||
});
|
||||
@@ -0,0 +1,987 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with no binding, in dropdown 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={Array []}
|
||||
appsEnabled={true}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={Array []}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isDropdown={true}
|
||||
theme={Object {}}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with no bindings 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={Array []}
|
||||
appsEnabled={true}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={Array []}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isDropdown={false}
|
||||
theme={Object {}}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with no extended component 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={Array []}
|
||||
appsEnabled={false}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={Array []}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isDropdown={false}
|
||||
theme={Object {}}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with no extended component, in dropdown 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={Array []}
|
||||
appsEnabled={false}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={Array []}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isDropdown={true}
|
||||
theme={Object {}}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with one binding 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={
|
||||
Array [
|
||||
Object {
|
||||
"app_id": "appid",
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/call/path",
|
||||
},
|
||||
},
|
||||
"hint": "Hint",
|
||||
"icon": "http://test.com/icon.png",
|
||||
"label": "Label",
|
||||
"location": "test",
|
||||
},
|
||||
]
|
||||
}
|
||||
appsEnabled={true}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={Array []}
|
||||
intl={
|
||||
Object {
|
||||
"formatMessage": [Function],
|
||||
}
|
||||
}
|
||||
isDropdown={false}
|
||||
theme={Object {}}
|
||||
>
|
||||
<li
|
||||
className="flex-parent--center"
|
||||
>
|
||||
<button
|
||||
className="navbar-toggle navbar-right__icon"
|
||||
id="appid_test"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="icon navbar-plugin-button"
|
||||
>
|
||||
<img
|
||||
height="16"
|
||||
src="http://test.com/icon.png"
|
||||
width="16"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</MobileChannelHeaderPlug>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with one binding, in dropdown 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={
|
||||
Array [
|
||||
Object {
|
||||
"app_id": "appid",
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/call/path",
|
||||
},
|
||||
},
|
||||
"hint": "Hint",
|
||||
"icon": "http://test.com/icon.png",
|
||||
"label": "Label",
|
||||
"location": "test",
|
||||
},
|
||||
]
|
||||
}
|
||||
appsEnabled={true}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={Array []}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isDropdown={true}
|
||||
theme={Object {}}
|
||||
>
|
||||
<li
|
||||
className="MenuItem"
|
||||
key="mobileChannelHeaderItemappidtest"
|
||||
role="presentation"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
role="menuitem"
|
||||
>
|
||||
Label
|
||||
</a>
|
||||
</li>
|
||||
</MobileChannelHeaderPlug>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with one extended component 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={Array []}
|
||||
appsEnabled={false}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={
|
||||
Array [
|
||||
Object {
|
||||
"action": [MockFunction],
|
||||
"dropdownText": "some dropdown text",
|
||||
"icon": <i
|
||||
className="fa fa-anchor"
|
||||
/>,
|
||||
"id": "someid",
|
||||
"pluginId": "pluginid",
|
||||
},
|
||||
]
|
||||
}
|
||||
intl={
|
||||
Object {
|
||||
"formatMessage": [Function],
|
||||
}
|
||||
}
|
||||
isDropdown={false}
|
||||
theme={Object {}}
|
||||
>
|
||||
<li
|
||||
className="flex-parent--center"
|
||||
>
|
||||
<button
|
||||
className="navbar-toggle navbar-right__icon"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="icon navbar-plugin-button"
|
||||
>
|
||||
<i
|
||||
className="fa fa-anchor"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</MobileChannelHeaderPlug>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with one extended component and one binding, in dropdown 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={
|
||||
Array [
|
||||
Object {
|
||||
"app_id": "appid",
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/call/path",
|
||||
},
|
||||
},
|
||||
"hint": "Hint",
|
||||
"icon": "http://test.com/icon.png",
|
||||
"label": "Label",
|
||||
"location": "test",
|
||||
},
|
||||
]
|
||||
}
|
||||
appsEnabled={true}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={
|
||||
Array [
|
||||
Object {
|
||||
"action": [MockFunction],
|
||||
"dropdownText": "some dropdown text",
|
||||
"icon": <i
|
||||
className="fa fa-anchor"
|
||||
/>,
|
||||
"id": "someid",
|
||||
"pluginId": "pluginid",
|
||||
},
|
||||
]
|
||||
}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isDropdown={true}
|
||||
theme={Object {}}
|
||||
>
|
||||
<li
|
||||
className="MenuItem"
|
||||
key="mobileChannelHeaderItemsomeid"
|
||||
role="presentation"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
role="menuitem"
|
||||
>
|
||||
some dropdown text
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className="MenuItem"
|
||||
key="mobileChannelHeaderItemappidtest"
|
||||
role="presentation"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
role="menuitem"
|
||||
>
|
||||
Label
|
||||
</a>
|
||||
</li>
|
||||
</MobileChannelHeaderPlug>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with one extended component, in dropdown 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={Array []}
|
||||
appsEnabled={false}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={
|
||||
Array [
|
||||
Object {
|
||||
"action": [MockFunction],
|
||||
"dropdownText": "some dropdown text",
|
||||
"icon": <i
|
||||
className="fa fa-anchor"
|
||||
/>,
|
||||
"id": "someid",
|
||||
"pluginId": "pluginid",
|
||||
},
|
||||
]
|
||||
}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isDropdown={true}
|
||||
theme={Object {}}
|
||||
>
|
||||
<li
|
||||
className="MenuItem"
|
||||
key="mobileChannelHeaderItemsomeid"
|
||||
role="presentation"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
role="menuitem"
|
||||
>
|
||||
some dropdown text
|
||||
</a>
|
||||
</li>
|
||||
</MobileChannelHeaderPlug>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with one extended components and one binding 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={
|
||||
Array [
|
||||
Object {
|
||||
"app_id": "appid",
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/call/path",
|
||||
},
|
||||
},
|
||||
"hint": "Hint",
|
||||
"icon": "http://test.com/icon.png",
|
||||
"label": "Label",
|
||||
"location": "test",
|
||||
},
|
||||
]
|
||||
}
|
||||
appsEnabled={true}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={
|
||||
Array [
|
||||
Object {
|
||||
"action": [MockFunction],
|
||||
"dropdownText": "some dropdown text",
|
||||
"icon": <i
|
||||
className="fa fa-anchor"
|
||||
/>,
|
||||
"id": "someid",
|
||||
"pluginId": "pluginid",
|
||||
},
|
||||
]
|
||||
}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isDropdown={false}
|
||||
theme={Object {}}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with two bindings 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={
|
||||
Array [
|
||||
Object {
|
||||
"app_id": "appid",
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/call/path",
|
||||
},
|
||||
},
|
||||
"hint": "Hint",
|
||||
"icon": "http://test.com/icon.png",
|
||||
"label": "Label",
|
||||
"location": "test",
|
||||
},
|
||||
Object {
|
||||
"app_id": "app2",
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/call/path",
|
||||
},
|
||||
},
|
||||
"hint": "Hint",
|
||||
"icon": "http://test.com/icon.png",
|
||||
"label": "Label",
|
||||
"location": "test",
|
||||
},
|
||||
]
|
||||
}
|
||||
appsEnabled={false}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={Array []}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isDropdown={false}
|
||||
theme={Object {}}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with two bindings, in dropdown 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={
|
||||
Array [
|
||||
Object {
|
||||
"app_id": "appid",
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/call/path",
|
||||
},
|
||||
},
|
||||
"hint": "Hint",
|
||||
"icon": "http://test.com/icon.png",
|
||||
"label": "Label",
|
||||
"location": "test",
|
||||
},
|
||||
Object {
|
||||
"app_id": "app2",
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/call/path",
|
||||
},
|
||||
},
|
||||
"hint": "Hint",
|
||||
"icon": "http://test.com/icon.png",
|
||||
"label": "Label",
|
||||
"location": "test",
|
||||
},
|
||||
]
|
||||
}
|
||||
appsEnabled={true}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={Array []}
|
||||
intl={
|
||||
Object {
|
||||
"formatMessage": [Function],
|
||||
}
|
||||
}
|
||||
isDropdown={true}
|
||||
theme={Object {}}
|
||||
>
|
||||
<li
|
||||
className="MenuItem"
|
||||
key="mobileChannelHeaderItemappidtest"
|
||||
role="presentation"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
role="menuitem"
|
||||
>
|
||||
Label
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className="MenuItem"
|
||||
key="mobileChannelHeaderItemapp2test"
|
||||
role="presentation"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
role="menuitem"
|
||||
>
|
||||
Label
|
||||
</a>
|
||||
</li>
|
||||
</MobileChannelHeaderPlug>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with two extended components 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={Array []}
|
||||
appsEnabled={false}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={
|
||||
Array [
|
||||
Object {
|
||||
"action": [MockFunction],
|
||||
"dropdownText": "some dropdown text",
|
||||
"icon": <i
|
||||
className="fa fa-anchor"
|
||||
/>,
|
||||
"id": "someid",
|
||||
"pluginId": "pluginid",
|
||||
},
|
||||
Object {
|
||||
"action": [MockFunction],
|
||||
"dropdownText": "some dropdown text",
|
||||
"icon": <i
|
||||
className="fa fa-anchor"
|
||||
/>,
|
||||
"id": "someid2",
|
||||
"pluginId": "pluginid",
|
||||
},
|
||||
]
|
||||
}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": "Etc/UTC",
|
||||
}
|
||||
}
|
||||
isDropdown={false}
|
||||
theme={Object {}}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`plugins/MobileChannelHeaderPlug should match snapshot with two extended components, in dropdown 1`] = `
|
||||
<MobileChannelHeaderPlug
|
||||
actions={
|
||||
Object {
|
||||
"handleBindingClick": [MockFunction],
|
||||
"openAppsModal": [MockFunction],
|
||||
"postEphemeralCallResponseForChannel": [MockFunction],
|
||||
}
|
||||
}
|
||||
appBindings={Array []}
|
||||
appsEnabled={false}
|
||||
channel={Object {}}
|
||||
channelMember={Object {}}
|
||||
components={
|
||||
Array [
|
||||
Object {
|
||||
"action": [MockFunction],
|
||||
"dropdownText": "some dropdown text",
|
||||
"icon": <i
|
||||
className="fa fa-anchor"
|
||||
/>,
|
||||
"id": "someid",
|
||||
"pluginId": "pluginid",
|
||||
},
|
||||
Object {
|
||||
"action": [MockFunction],
|
||||
"dropdownText": "some dropdown text",
|
||||
"icon": <i
|
||||
className="fa fa-anchor"
|
||||
/>,
|
||||
"id": "someid2",
|
||||
"pluginId": "pluginid",
|
||||
},
|
||||
]
|
||||
}
|
||||
intl={
|
||||
Object {
|
||||
"formatMessage": [Function],
|
||||
}
|
||||
}
|
||||
isDropdown={true}
|
||||
theme={Object {}}
|
||||
>
|
||||
<li
|
||||
className="MenuItem"
|
||||
key="mobileChannelHeaderItemsomeid"
|
||||
role="presentation"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
role="menuitem"
|
||||
>
|
||||
some dropdown text
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className="MenuItem"
|
||||
key="mobileChannelHeaderItemsomeid2"
|
||||
role="presentation"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
role="menuitem"
|
||||
>
|
||||
some dropdown text
|
||||
</a>
|
||||
</li>
|
||||
</MobileChannelHeaderPlug>
|
||||
`;
|
||||
49
webapp/channels/src/plugins/mobile_channel_header_plug/index.ts
Обычный файл
49
webapp/channels/src/plugins/mobile_channel_header_plug/index.ts
Обычный файл
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {appsEnabled, makeAppBindingsSelector} from 'mattermost-redux/selectors/entities/apps';
|
||||
import {AppBindingLocations} from 'mattermost-redux/constants/apps';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||
import {handleBindingClick, openAppsModal, postEphemeralCallResponseForChannel} from 'actions/apps';
|
||||
|
||||
import {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForChannel} from 'types/apps';
|
||||
|
||||
import MobileChannelHeaderPlug from './mobile_channel_header_plug';
|
||||
|
||||
const getChannelHeaderBindings = makeAppBindingsSelector(AppBindingLocations.CHANNEL_HEADER_ICON);
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const apps = appsEnabled(state);
|
||||
return {
|
||||
appBindings: getChannelHeaderBindings(state),
|
||||
appsEnabled: apps,
|
||||
channelMember: getMyCurrentChannelMembership(state),
|
||||
components: state.plugins.components.MobileChannelHeaderButton,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
handleBindingClick: HandleBindingClick;
|
||||
postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel;
|
||||
openAppsModal: OpenAppsModal;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<any>, Actions>({
|
||||
handleBindingClick,
|
||||
postEphemeralCallResponseForChannel,
|
||||
openAppsModal,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MobileChannelHeaderPlug);
|
||||
@@ -0,0 +1,472 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {mount} from 'enzyme';
|
||||
|
||||
import MobileChannelHeaderPlug, {RawMobileChannelHeaderPlug} from 'plugins/mobile_channel_header_plug/mobile_channel_header_plug';
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {createCallContext} from 'utils/apps';
|
||||
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
|
||||
import {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
describe('plugins/MobileChannelHeaderPlug', () => {
|
||||
const testPlug = {
|
||||
id: 'someid',
|
||||
pluginId: 'pluginid',
|
||||
icon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn(),
|
||||
dropdownText: 'some dropdown text',
|
||||
};
|
||||
|
||||
const testBinding = {
|
||||
app_id: 'appid',
|
||||
location: 'test',
|
||||
icon: 'http://test.com/icon.png',
|
||||
label: 'Label',
|
||||
hint: 'Hint',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/call/path',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const testChannel = {} as Channel;
|
||||
const testChannelMember = {} as ChannelMembership;
|
||||
const testTheme = {} as Theme;
|
||||
const intl = {
|
||||
formatMessage: (message: {id: string; defaultMessage: string}) => {
|
||||
return message.defaultMessage;
|
||||
},
|
||||
} as any;
|
||||
|
||||
test('should match snapshot with no extended component', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with one extended component', () => {
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[testPlug]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a single list item containing a button
|
||||
expect(wrapper.find('li')).toHaveLength(1);
|
||||
expect(wrapper.find('button')).toHaveLength(1);
|
||||
|
||||
wrapper.instance().fireAction = jest.fn();
|
||||
wrapper.find('button').first().simulate('click');
|
||||
expect(wrapper.instance().fireAction).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.instance().fireAction).toBeCalledWith(testPlug);
|
||||
});
|
||||
|
||||
test('should match snapshot with two extended components', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[testPlug, {...testPlug, id: 'someid2'}]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with no bindings', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={true}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with one binding', () => {
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a single list item containing a button
|
||||
expect(wrapper.find('li')).toHaveLength(1);
|
||||
expect(wrapper.find('button')).toHaveLength(1);
|
||||
|
||||
wrapper.instance().fireAppAction = jest.fn();
|
||||
wrapper.find('button').first().simulate('click');
|
||||
expect(wrapper.instance().fireAppAction).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.instance().fireAppAction).toBeCalledWith(testBinding);
|
||||
});
|
||||
|
||||
test('should match snapshot with two bindings', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={false}
|
||||
appBindings={[testBinding, {...testBinding, app_id: 'app2'}]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with one extended components and one binding', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[testPlug]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with no extended component, in dropdown', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with one extended component, in dropdown', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[testPlug]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a single list item containing an anchor
|
||||
expect(wrapper.find('li')).toHaveLength(1);
|
||||
expect(wrapper.find('a')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should match snapshot with two extended components, in dropdown', () => {
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[testPlug, {...testPlug, id: 'someid2'}]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a two list items containing anchors
|
||||
expect(wrapper.find('li')).toHaveLength(2);
|
||||
expect(wrapper.find('a')).toHaveLength(2);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
instance.fireAction = jest.fn();
|
||||
|
||||
wrapper.find('a').first().simulate('click');
|
||||
expect(instance.fireAction).toHaveBeenCalledTimes(1);
|
||||
expect(instance.fireAction).toBeCalledWith(testPlug);
|
||||
});
|
||||
|
||||
test('should match snapshot with no binding, in dropdown', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={true}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with one binding, in dropdown', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a single list item containing an anchor
|
||||
expect(wrapper.find('li')).toHaveLength(1);
|
||||
expect(wrapper.find('a')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should match snapshot with two bindings, in dropdown', () => {
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding, {...testBinding, app_id: 'app2'}]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a two list items containing anchors
|
||||
expect(wrapper.find('li')).toHaveLength(2);
|
||||
expect(wrapper.find('a')).toHaveLength(2);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
instance.fireAppAction = jest.fn();
|
||||
|
||||
wrapper.find('a').first().simulate('click');
|
||||
expect(instance.fireAppAction).toHaveBeenCalledTimes(1);
|
||||
expect(instance.fireAppAction).toBeCalledWith(testBinding);
|
||||
});
|
||||
|
||||
test('should match snapshot with one extended component and one binding, in dropdown', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[testPlug]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a two list items containing anchors
|
||||
expect(wrapper.find('li')).toHaveLength(2);
|
||||
expect(wrapper.find('a')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should call plugin.action on fireAction', () => {
|
||||
const channel = {id: 'channel_id'} as Channel;
|
||||
const channelMember = {} as ChannelMembership;
|
||||
const newTestPlug = {
|
||||
id: 'someid',
|
||||
pluginId: 'pluginid',
|
||||
icon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn(),
|
||||
dropdownText: 'some dropdown text',
|
||||
};
|
||||
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[newTestPlug]}
|
||||
channel={channel}
|
||||
channelMember={channelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper.instance().fireAction(newTestPlug);
|
||||
expect(newTestPlug.action).toHaveBeenCalledTimes(1);
|
||||
expect(newTestPlug.action).toBeCalledWith(channel, channelMember);
|
||||
});
|
||||
|
||||
test('should call handleBindingClick on fireAppAction', () => {
|
||||
const channel = {id: 'channel_id'} as Channel;
|
||||
const channelMember = {} as ChannelMembership;
|
||||
|
||||
const handleBindingClick = jest.fn().mockResolvedValue({data: {type: AppCallResponseTypes.OK}});
|
||||
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={channel}
|
||||
channelMember={channelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding]}
|
||||
actions={{
|
||||
handleBindingClick,
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
|
||||
const context = createCallContext(
|
||||
testBinding.app_id,
|
||||
testBinding.location,
|
||||
channel.id,
|
||||
channel.team_id,
|
||||
);
|
||||
|
||||
wrapper.instance().fireAppAction(testBinding);
|
||||
expect(handleBindingClick).toHaveBeenCalledTimes(1);
|
||||
expect(handleBindingClick).toBeCalledWith(testBinding, context, expect.anything());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {injectIntl, IntlShape} from 'react-intl';
|
||||
|
||||
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
|
||||
import {AppBinding} from '@mattermost/types/apps';
|
||||
import {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {PluginComponent} from 'types/store/plugins';
|
||||
import {createCallContext} from 'utils/apps';
|
||||
import {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForChannel} from 'types/apps';
|
||||
|
||||
type Props = {
|
||||
|
||||
/*
|
||||
* Components or actions to add as channel header buttons
|
||||
*/
|
||||
components?: PluginComponent[];
|
||||
|
||||
/*
|
||||
* Set to true if the plug is in the dropdown
|
||||
*/
|
||||
isDropdown: boolean;
|
||||
channel: Channel;
|
||||
channelMember?: ChannelMembership;
|
||||
|
||||
/*
|
||||
* Logged in user's theme
|
||||
*/
|
||||
theme: Theme;
|
||||
appBindings: AppBinding[];
|
||||
appsEnabled: boolean;
|
||||
intl: IntlShape;
|
||||
actions: {
|
||||
handleBindingClick: HandleBindingClick;
|
||||
postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel;
|
||||
openAppsModal: OpenAppsModal;
|
||||
};
|
||||
}
|
||||
|
||||
class MobileChannelHeaderPlug extends React.PureComponent<Props> {
|
||||
createAppButton = (binding: AppBinding) => {
|
||||
const onClick = () => this.fireAppAction(binding);
|
||||
|
||||
if (this.props.isDropdown) {
|
||||
return (
|
||||
<li
|
||||
key={'mobileChannelHeaderItem' + binding.app_id + binding.location}
|
||||
role='presentation'
|
||||
className='MenuItem'
|
||||
>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={onClick}
|
||||
>
|
||||
{binding.label}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className='flex-parent--center'>
|
||||
<button
|
||||
id={`${binding.app_id}_${binding.location}`}
|
||||
className='navbar-toggle navbar-right__icon'
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className='icon navbar-plugin-button'>
|
||||
<img
|
||||
src={binding.icon}
|
||||
width='16'
|
||||
height='16'
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
createButton = (plug: PluginComponent) => {
|
||||
const onClick = () => this.fireAction(plug);
|
||||
|
||||
if (this.props.isDropdown) {
|
||||
return (
|
||||
<li
|
||||
key={'mobileChannelHeaderItem' + plug.id}
|
||||
role='presentation'
|
||||
className='MenuItem'
|
||||
>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={onClick}
|
||||
>
|
||||
{plug.dropdownText}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className='flex-parent--center'>
|
||||
<button
|
||||
className='navbar-toggle navbar-right__icon'
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className='icon navbar-plugin-button'>
|
||||
{plug.icon}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
createList(plugs: PluginComponent[]) {
|
||||
return plugs.map(this.createButton);
|
||||
}
|
||||
|
||||
createAppList(bindings: AppBinding[]) {
|
||||
return bindings.map(this.createAppButton);
|
||||
}
|
||||
|
||||
fireAction(plug: PluginComponent) {
|
||||
return plug.action?.(this.props.channel, this.props.channelMember);
|
||||
}
|
||||
|
||||
fireAppAction = async (binding: AppBinding) => {
|
||||
const {channel, intl} = this.props;
|
||||
const context = createCallContext(
|
||||
binding.app_id,
|
||||
binding.location,
|
||||
channel.id,
|
||||
channel.team_id,
|
||||
);
|
||||
|
||||
const res = await this.props.actions.handleBindingClick(binding, context, intl);
|
||||
|
||||
if (res.error) {
|
||||
const errorResponse = res.error;
|
||||
const errorMessage = errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error occurred.',
|
||||
});
|
||||
this.props.actions.postEphemeralCallResponseForChannel(errorResponse, errorMessage, channel.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const callResp = res.data!;
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.text) {
|
||||
this.props.actions.postEphemeralCallResponseForChannel(callResp, callResp.text, channel.id);
|
||||
}
|
||||
break;
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
break;
|
||||
case AppCallResponseTypes.FORM:
|
||||
if (callResp.form) {
|
||||
this.props.actions.openAppsModal(callResp.form, context);
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
const errorMessage = this.props.intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {
|
||||
type: callResp.type,
|
||||
});
|
||||
this.props.actions.postEphemeralCallResponseForChannel(callResp, errorMessage, channel.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const components = this.props.components || [];
|
||||
const bindings = this.props.appBindings || [];
|
||||
|
||||
if (components.length === 0 && bindings.length === 0) {
|
||||
return null;
|
||||
} else if (components.length === 1 && bindings.length === 0) {
|
||||
return this.createButton(components[0]);
|
||||
} else if (components.length === 0 && bindings.length === 1) {
|
||||
return this.createAppButton(bindings[0]);
|
||||
}
|
||||
|
||||
if (!this.props.isDropdown) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const plugItems = this.createList(components);
|
||||
const appItems = this.createAppList(bindings);
|
||||
return (<>
|
||||
{plugItems}
|
||||
{appItems}
|
||||
</>);
|
||||
}
|
||||
}
|
||||
|
||||
// Exported for tests
|
||||
export {MobileChannelHeaderPlug as RawMobileChannelHeaderPlug};
|
||||
|
||||
export default injectIntl(MobileChannelHeaderPlug);
|
||||
@@ -0,0 +1,239 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`plugins/Pluggable should match snapshot with extended component 1`] = `
|
||||
<Pluggable
|
||||
components={
|
||||
Object {
|
||||
"PopoverSection1": Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
pluggableName="PopoverSection1"
|
||||
theme={Object {}}
|
||||
>
|
||||
<PluggableErrorBoundary
|
||||
key="PopoverSection1undefined"
|
||||
>
|
||||
<ProfilePopoverPlugin
|
||||
theme={Object {}}
|
||||
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 {},
|
||||
"reconnectCallback": null,
|
||||
"reconnectListeners": Set {},
|
||||
"responseCallbacks": Object {},
|
||||
"responseSequence": 1,
|
||||
"serverSequence": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
id="pluginId"
|
||||
>
|
||||
ProfilePopoverPlugin
|
||||
</span>
|
||||
</ProfilePopoverPlugin>
|
||||
</PluggableErrorBoundary>
|
||||
</Pluggable>
|
||||
`;
|
||||
|
||||
exports[`plugins/Pluggable should match snapshot with extended component with pluggableName 1`] = `
|
||||
<Pluggable
|
||||
components={
|
||||
Object {
|
||||
"PopoverSection1": Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
pluggableName="PopoverSection1"
|
||||
theme={Object {}}
|
||||
>
|
||||
<PluggableErrorBoundary
|
||||
key="PopoverSection1undefined"
|
||||
>
|
||||
<ProfilePopoverPlugin
|
||||
theme={Object {}}
|
||||
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 {},
|
||||
"reconnectCallback": null,
|
||||
"reconnectListeners": Set {},
|
||||
"responseCallbacks": Object {},
|
||||
"responseSequence": 1,
|
||||
"serverSequence": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
id="pluginId"
|
||||
>
|
||||
ProfilePopoverPlugin
|
||||
</span>
|
||||
</ProfilePopoverPlugin>
|
||||
</PluggableErrorBoundary>
|
||||
</Pluggable>
|
||||
`;
|
||||
|
||||
exports[`plugins/Pluggable should match snapshot with no extended component 1`] = `
|
||||
<Pluggable
|
||||
components={Object {}}
|
||||
pluggableName=""
|
||||
theme={Object {}}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`plugins/Pluggable should match snapshot with non-null pluggableId 1`] = `
|
||||
<Pluggable
|
||||
components={
|
||||
Object {
|
||||
"PopoverSection1": Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
pluggableId="pluggableId"
|
||||
pluggableName="PopoverSection1"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`plugins/Pluggable should match snapshot with null pluggableId 1`] = `
|
||||
<Pluggable
|
||||
components={
|
||||
Object {
|
||||
"PopoverSection1": Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
pluggableName="PopoverSection1"
|
||||
theme={Object {}}
|
||||
>
|
||||
<PluggableErrorBoundary
|
||||
key="PopoverSection1undefined"
|
||||
>
|
||||
<ProfilePopoverPlugin
|
||||
theme={Object {}}
|
||||
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 {},
|
||||
"reconnectCallback": null,
|
||||
"reconnectListeners": Set {},
|
||||
"responseCallbacks": Object {},
|
||||
"responseSequence": 1,
|
||||
"serverSequence": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
id="pluginId"
|
||||
>
|
||||
ProfilePopoverPlugin
|
||||
</span>
|
||||
</ProfilePopoverPlugin>
|
||||
</PluggableErrorBoundary>
|
||||
</Pluggable>
|
||||
`;
|
||||
|
||||
exports[`plugins/Pluggable should match snapshot with valid pluggableId 1`] = `
|
||||
<Pluggable
|
||||
components={
|
||||
Object {
|
||||
"PopoverSection1": Array [
|
||||
Object {
|
||||
"component": [Function],
|
||||
"id": "pluggableId",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
pluggableId="pluggableId"
|
||||
pluggableName="PopoverSection1"
|
||||
theme={Object {}}
|
||||
>
|
||||
<PluggableErrorBoundary
|
||||
key="PopoverSection1pluggableId"
|
||||
>
|
||||
<ProfilePopoverPlugin
|
||||
theme={Object {}}
|
||||
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 {},
|
||||
"reconnectCallback": null,
|
||||
"reconnectListeners": Set {},
|
||||
"responseCallbacks": Object {},
|
||||
"responseSequence": 1,
|
||||
"serverSequence": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
id="pluginId"
|
||||
>
|
||||
ProfilePopoverPlugin
|
||||
</span>
|
||||
</ProfilePopoverPlugin>
|
||||
</PluggableErrorBoundary>
|
||||
</Pluggable>
|
||||
`;
|
||||
84
webapp/channels/src/plugins/pluggable/error_boundary.tsx
Обычный файл
84
webapp/channels/src/plugins/pluggable/error_boundary.tsx
Обычный файл
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
pluginId?: string;
|
||||
}
|
||||
|
||||
type State = {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
const WrapperDiv = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
#root > & {
|
||||
// prevent root layout error; fit into announcement area
|
||||
grid-area: announcement;
|
||||
display: flex;
|
||||
word-break: normal;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
height: 40px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default class PluggableErrorBoundary extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
hasError: false,
|
||||
};
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return {
|
||||
hasError: true,
|
||||
};
|
||||
}
|
||||
|
||||
clearErrorState = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.setState({hasError: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<WrapperDiv>
|
||||
<FormattedMessage
|
||||
id='pluggable.errorOccurred'
|
||||
defaultMessage='An error occurred in the {pluginId} plugin.'
|
||||
values={{
|
||||
pluginId: this.props.pluginId,
|
||||
}}
|
||||
/>
|
||||
<br/>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.clearErrorState}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='pluggable.errorRefresh'
|
||||
defaultMessage='Refresh?'
|
||||
values={{
|
||||
pluginId: this.props.pluginId,
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</WrapperDiv>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
19
webapp/channels/src/plugins/pluggable/index.ts
Обычный файл
19
webapp/channels/src/plugins/pluggable/index.ts
Обычный файл
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import Pluggable from './pluggable';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
components: state.plugins.components,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Pluggable);
|
||||
124
webapp/channels/src/plugins/pluggable/pluggable.test.jsx
Обычный файл
124
webapp/channels/src/plugins/pluggable/pluggable.test.jsx
Обычный файл
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
import Pluggable from './pluggable';
|
||||
|
||||
class ProfilePopoverPlugin extends React.PureComponent {
|
||||
render() {
|
||||
return <span id='pluginId'>{'ProfilePopoverPlugin'}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('actions/views/profile_popover');
|
||||
|
||||
describe('plugins/Pluggable', () => {
|
||||
const baseProps = {
|
||||
pluggableName: '',
|
||||
components: {},
|
||||
theme: {},
|
||||
};
|
||||
|
||||
test('should match snapshot with no extended component', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Pluggable
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with extended component', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Pluggable
|
||||
{...baseProps}
|
||||
pluggableName='PopoverSection1'
|
||||
components={{PopoverSection1: [{component: ProfilePopoverPlugin}]}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('#pluginId').text()).toBe('ProfilePopoverPlugin');
|
||||
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('should match snapshot with extended component with pluggableName', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Pluggable
|
||||
{...baseProps}
|
||||
pluggableName='PopoverSection1'
|
||||
components={{PopoverSection1: [{component: ProfilePopoverPlugin}]}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('#pluginId').text()).toBe('ProfilePopoverPlugin');
|
||||
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('should return null if neither pluggableName nor children is is defined in props', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Pluggable
|
||||
{...baseProps}
|
||||
components={{PopoverSection1: [{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', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Pluggable
|
||||
pluggableName='PopoverSection1'
|
||||
pluggableId={'pluggableId'}
|
||||
components={{PopoverSection1: [{component: ProfilePopoverPlugin}]}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with null pluggableId', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Pluggable
|
||||
{...baseProps}
|
||||
pluggableName='PopoverSection1'
|
||||
components={{PopoverSection1: [{component: ProfilePopoverPlugin}]}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('should match snapshot with valid pluggableId', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<Pluggable
|
||||
{...baseProps}
|
||||
pluggableName='PopoverSection1'
|
||||
pluggableId={'pluggableId'}
|
||||
components={{PopoverSection1: [{id: 'pluggableId', component: ProfilePopoverPlugin}]}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find(ProfilePopoverPlugin).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
130
webapp/channels/src/plugins/pluggable/pluggable.tsx
Обычный файл
130
webapp/channels/src/plugins/pluggable/pluggable.tsx
Обычный файл
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {WebSocketClient} from '@mattermost/client';
|
||||
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {ProductComponent} from 'types/store/plugins';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import webSocketClient from 'client/web_websocket_client';
|
||||
|
||||
import PluggableErrorBoundary from './error_boundary';
|
||||
|
||||
type Props = {
|
||||
|
||||
/*
|
||||
* Override the component to be plugged
|
||||
*/
|
||||
pluggableName: string;
|
||||
|
||||
/*
|
||||
* Components for overriding provided by plugins
|
||||
*/
|
||||
components: GlobalState['plugins']['components'];
|
||||
|
||||
/*
|
||||
* Logged in user's theme
|
||||
*/
|
||||
theme: Theme;
|
||||
|
||||
/*
|
||||
* Id of the specific component to be plugged.
|
||||
*/
|
||||
pluggableId?: string;
|
||||
|
||||
/*
|
||||
* Name of the sub component to use. Defaults to 'component' if unspecified.
|
||||
*
|
||||
* Only supported when pluggableName is "Product".
|
||||
*/
|
||||
subComponentName?: 'mainComponent' | 'headerCentreComponent' | 'headerRightComponent';
|
||||
|
||||
/*
|
||||
* Accept any other prop to pass onto the plugin component
|
||||
*/
|
||||
[name: string]: any;
|
||||
}
|
||||
|
||||
type BaseChildProps = {
|
||||
theme: Theme;
|
||||
webSocketClient?: WebSocketClient;
|
||||
}
|
||||
|
||||
export default function Pluggable(props: Props): JSX.Element | null {
|
||||
const {
|
||||
components,
|
||||
pluggableId,
|
||||
pluggableName,
|
||||
subComponentName = '',
|
||||
theme,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
if (!pluggableName || !Object.hasOwnProperty.call(components, pluggableName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let pluginComponents = components[pluggableName]!;
|
||||
|
||||
if (pluggableId) {
|
||||
pluginComponents = pluginComponents.filter(
|
||||
(element) => element.id === pluggableId);
|
||||
}
|
||||
|
||||
// Override the default component with any registered plugin's component
|
||||
// Select a specific component by pluginId if available
|
||||
let content;
|
||||
|
||||
if (pluggableName === 'Product') {
|
||||
content = (pluginComponents as ProductComponent[]).map((pc) => {
|
||||
if (!subComponentName || !pc[subComponentName]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Component = pc[subComponentName]! as React.ComponentType<BaseChildProps>;
|
||||
|
||||
return (
|
||||
<PluggableErrorBoundary
|
||||
key={pluggableName + pc.id}
|
||||
pluginId={pc.pluginId}
|
||||
>
|
||||
<Component
|
||||
{...otherProps}
|
||||
theme={theme}
|
||||
/>
|
||||
</PluggableErrorBoundary>
|
||||
);
|
||||
});
|
||||
} else {
|
||||
content = pluginComponents.map((p) => {
|
||||
if (!p.component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Component = p.component as React.ComponentType<BaseChildProps>;
|
||||
|
||||
return (
|
||||
<PluggableErrorBoundary
|
||||
key={pluggableName + p.id}
|
||||
pluginId={p.pluginId}
|
||||
>
|
||||
<Component
|
||||
{...otherProps}
|
||||
theme={theme}
|
||||
webSocketClient={webSocketClient}
|
||||
/>
|
||||
</PluggableErrorBoundary>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{content}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
44
webapp/channels/src/plugins/products.test.ts
Обычный файл
44
webapp/channels/src/plugins/products.test.ts
Обычный файл
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import testConfigureStore from 'tests/test_store';
|
||||
|
||||
import {initializeProducts} from './products';
|
||||
|
||||
(window as any).REMOTE_CONTAINERS = {};
|
||||
|
||||
describe('initializeProducts', () => {
|
||||
test('should set Client4 to use the correct Boards URL for product mode', async () => {
|
||||
const store = testConfigureStore({
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
FeatureFlagBoardsProduct: 'true',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await store.dispatch(initializeProducts());
|
||||
|
||||
expect(Client4.getBoardsRoute().startsWith('/plugins/boards')).toBe(true);
|
||||
});
|
||||
|
||||
test('should set Client4 to use the correct Boards URL for plugin mode', async () => {
|
||||
const store = testConfigureStore({
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
FeatureFlagBoardsProduct: 'false',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await store.dispatch(initializeProducts());
|
||||
|
||||
expect(Client4.getBoardsRoute().startsWith('/plugins/focalboard')).toBe(true);
|
||||
});
|
||||
});
|
||||
126
webapp/channels/src/plugins/products.ts
Обычный файл
126
webapp/channels/src/plugins/products.ts
Обычный файл
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Store} from 'redux';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import store from 'stores/redux_store';
|
||||
|
||||
import PluginRegistry from './registry';
|
||||
|
||||
export abstract class ProductPlugin {
|
||||
abstract initialize(registry: PluginRegistry, store: Store): void;
|
||||
abstract uninitialize(): void;
|
||||
}
|
||||
|
||||
export function initializeProducts() {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return Promise.all([
|
||||
dispatch(loadRemoteModules()),
|
||||
dispatch(configureClient()),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
function configureClient() {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const config = getConfig(getState());
|
||||
|
||||
Client4.setUseBoardsProduct(config.FeatureFlagBoardsProduct === 'true');
|
||||
|
||||
return Promise.resolve({data: true});
|
||||
};
|
||||
}
|
||||
|
||||
function loadRemoteModules() {
|
||||
/* eslint-disable no-console */
|
||||
return async (/*dispatch: DispatchFunc, getState: GetStateFunc*/) => {
|
||||
// const config = getConfig(getState());
|
||||
|
||||
/**
|
||||
* products contains a map of product IDs to a function that will load all of their parts. Calling that
|
||||
* function will return an object where each field is a Promise that will resolve to that module.
|
||||
*
|
||||
* Note that these import paths must be statically defined or else they won't be found at runtime. They
|
||||
* can't be constructed based on the name of a product at runtime.
|
||||
*/
|
||||
const products = [
|
||||
{
|
||||
id: 'boards',
|
||||
load: () => ({
|
||||
index: import('boards'),
|
||||
|
||||
// manifest: import('boards/manifest'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'playbooks',
|
||||
load: () => ({
|
||||
index: import('playbooks'),
|
||||
|
||||
// manifest: import('boards/manifest'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
await Promise.all(products.map(async (product) => {
|
||||
if (!REMOTE_CONTAINERS[product.id]) {
|
||||
console.log(`Product ${product.id} not found. Not loading it.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Loading product ${product.id}...`);
|
||||
|
||||
// Start loading the product
|
||||
let imports;
|
||||
try {
|
||||
imports = product.load();
|
||||
} catch (e) {
|
||||
console.error(`Error loading ${product.id}`, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for the individual parts to load
|
||||
let index;
|
||||
try {
|
||||
index = await imports.index;
|
||||
} catch (e) {
|
||||
console.error(`Error loading index for ${product.id}`, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// let manifest;
|
||||
// try {
|
||||
// manifest = await imports.manifest;
|
||||
// } catch (e) {
|
||||
// console.error(`Error loading manifest for ${product.id}`, e);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Initialize the previously loaded data
|
||||
console.log(`Initializing product ${product.id}...`);
|
||||
|
||||
try {
|
||||
initializeProduct(product.id, index.default);
|
||||
} catch (e) {
|
||||
console.error(`Error loading and initializing product ${product.id}`, e);
|
||||
}
|
||||
|
||||
console.log(`Product ${product.id} initialized!`);
|
||||
}));
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
|
||||
function initializeProduct(id: string, Product: new () => ProductPlugin) {
|
||||
const plugin = new Product();
|
||||
const registry = new PluginRegistry(id);
|
||||
|
||||
plugin.initialize(registry, store);
|
||||
}
|
||||
1165
webapp/channels/src/plugins/registry.ts
Обычный файл
1165
webapp/channels/src/plugins/registry.ts
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {Placement} from 'tippy.js';
|
||||
|
||||
import {setAutoShowLinkedBoardPreference} from 'mattermost-redux/actions/boards';
|
||||
|
||||
import {TourTip} from '@mattermost/components';
|
||||
import {shouldShowAutoLinkedBoard} from 'selectors/plugins';
|
||||
import {suitePluginIds} from 'utils/constants';
|
||||
import {getPluggableId} from 'selectors/rhs';
|
||||
import {PluginComponent} from 'types/store/plugins';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
type Props = {
|
||||
pulsatingDotPlacement?: Omit<Placement, 'auto'| 'auto-end'>;
|
||||
}
|
||||
|
||||
const AutoShowLinkedBoardTourTip = ({
|
||||
pulsatingDotPlacement = 'auto',
|
||||
}: Props): JSX.Element | null => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const rhsPlugins: PluginComponent[] = useSelector((state: GlobalState) => state.plugins.components.RightHandSidebarComponent);
|
||||
const pluggableId = useSelector(getPluggableId);
|
||||
const pluginComponent = rhsPlugins.find((element: PluginComponent) => element.id === pluggableId);
|
||||
|
||||
const isBoards = pluginComponent && (pluginComponent.pluginId === suitePluginIds.focalboard || pluginComponent.pluginId === suitePluginIds.boards);
|
||||
const showAutoLinkedBoard = useSelector(shouldShowAutoLinkedBoard);
|
||||
const showAutoLinkedBoardTourTip = isBoards && showAutoLinkedBoard;
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='autoShowLinkedBoard.tutorialTip.title'
|
||||
defaultMessage='Link kanban boards to channels'
|
||||
/>
|
||||
);
|
||||
|
||||
const screen = (
|
||||
<FormattedMessage
|
||||
id='autoShowLinkedBoard.tutorialTip.description'
|
||||
defaultMessage='Manage tasks, plan sprints, conduct standup with the help of kanban boards and tables.'
|
||||
/>
|
||||
);
|
||||
|
||||
const handleDismiss = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setAutoShowLinkedBoardPreference());
|
||||
}, []);
|
||||
|
||||
const handleOpen = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
dispatch(setAutoShowLinkedBoardPreference());
|
||||
}, []);
|
||||
|
||||
const nextBtn = (
|
||||
<FormattedMessage
|
||||
id={'tutorial_tip.done'}
|
||||
defaultMessage={'Done'}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!showAutoLinkedBoardTourTip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TourTip
|
||||
show={true}
|
||||
screen={screen}
|
||||
title={title}
|
||||
overlayPunchOut={null}
|
||||
placement='left-start'
|
||||
pulsatingDotPlacement={pulsatingDotPlacement}
|
||||
step={1}
|
||||
singleTip={true}
|
||||
showOptOut={false}
|
||||
interactivePunchOut={true}
|
||||
handleDismiss={handleDismiss}
|
||||
handleOpen={handleOpen}
|
||||
handlePrevious={handleDismiss}
|
||||
pulsatingDotTranslate={{x: -10, y: 70}}
|
||||
tippyBlueStyle={true}
|
||||
hideBackdrop={true}
|
||||
nextBtn={nextBtn}
|
||||
handleNext={handleDismiss}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoShowLinkedBoardTourTip;
|
||||
26
webapp/channels/src/plugins/rhs_plugin/index.ts
Обычный файл
26
webapp/channels/src/plugins/rhs_plugin/index.ts
Обычный файл
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {PluginComponent} from 'types/store/plugins';
|
||||
|
||||
import {getPluggableId} from 'selectors/rhs';
|
||||
|
||||
import RHSPlugin from './rhs_plugin';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const rhsPlugins: PluginComponent[] = state.plugins.components.RightHandSidebarComponent;
|
||||
const pluggableId = getPluggableId(state);
|
||||
const pluginComponent = rhsPlugins.find((element: PluginComponent) => element.id === pluggableId);
|
||||
const pluginTitle = pluginComponent ? pluginComponent.title : '';
|
||||
|
||||
return {
|
||||
showPluggable: Boolean(pluginComponent),
|
||||
pluggableId,
|
||||
title: pluginTitle,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(RHSPlugin);
|
||||
48
webapp/channels/src/plugins/rhs_plugin/rhs_plugin.tsx
Обычный файл
48
webapp/channels/src/plugins/rhs_plugin/rhs_plugin.tsx
Обычный файл
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import SearchResultsHeader from 'components/search_results_header';
|
||||
import {BoardsTourTip, PlaybooksTourTip} from 'components/tours/worktemplate_explore_tour';
|
||||
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
|
||||
import AutoShowLinkedBoardTourTip from './auto_show_linked_board_tourtip';
|
||||
|
||||
export type Props = {
|
||||
showPluggable: boolean;
|
||||
pluggableId: string;
|
||||
title: React.ReactNode;
|
||||
}
|
||||
|
||||
export default class RhsPlugin extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const boardsTourTip = (<BoardsTourTip/>);
|
||||
const playbooksTourtip = (<PlaybooksTourTip/>);
|
||||
const autoLinkedBoardTourTip = (<AutoShowLinkedBoardTourTip/>);
|
||||
|
||||
return (
|
||||
<div
|
||||
id='rhsContainer'
|
||||
className='sidebar-right__body'
|
||||
>
|
||||
<SearchResultsHeader>
|
||||
{autoLinkedBoardTourTip}
|
||||
{this.props.title}
|
||||
</SearchResultsHeader>
|
||||
{
|
||||
this.props.showPluggable &&
|
||||
<>
|
||||
<Pluggable
|
||||
pluggableName='RightHandSidebarComponent'
|
||||
pluggableId={this.props.pluggableId}
|
||||
/>
|
||||
{boardsTourTip}
|
||||
{playbooksTourtip}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`plugins/MainMenuActions should match snapshot in mobile view with some plugin and ability to click plugin 1`] = `
|
||||
<div
|
||||
aria-label="main menu"
|
||||
className="a11y__popup Menu"
|
||||
role="menu"
|
||||
>
|
||||
<ul
|
||||
className="Menu__content dropdown-menu"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
<MenuGroup>
|
||||
<Connect(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"sysconsole_write_billing",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuCloudTrial
|
||||
id="menuCloudTrial"
|
||||
/>
|
||||
</Connect(SystemPermissionGate)>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<Connect(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"sysconsole_write_about_edition_and_license",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuStartTrial
|
||||
id="startTrial"
|
||||
/>
|
||||
</Connect(SystemPermissionGate)>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItemAction
|
||||
icon={
|
||||
<i
|
||||
className="mentions"
|
||||
>
|
||||
@
|
||||
</i>
|
||||
}
|
||||
id="recentMentions"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Recent Mentions"
|
||||
/>
|
||||
<MenuItemAction
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-bookmark"
|
||||
/>
|
||||
}
|
||||
id="flaggedPosts"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Saved Posts"
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"isContentProductSettings": false,
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-user"
|
||||
/>
|
||||
}
|
||||
id="profileSettings"
|
||||
modalId="user_settings"
|
||||
show={true}
|
||||
text="Profile"
|
||||
/>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"isContentProductSettings": true,
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-cog"
|
||||
/>
|
||||
}
|
||||
id="accountSettings"
|
||||
modalId="user_settings"
|
||||
show={true}
|
||||
text="Settings"
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<Connect(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-user-plus"
|
||||
/>
|
||||
}
|
||||
id="addGroupsToTeam"
|
||||
modalId="add_groups_to_team"
|
||||
show={true}
|
||||
text="Add Groups to Team"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
<Connect(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"add_user_to_team",
|
||||
"invite_guest",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
extraText="Add people to the team"
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-user-plus"
|
||||
/>
|
||||
}
|
||||
id="invitePeople"
|
||||
modalId="invitation"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Invite People"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<Connect(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-globe"
|
||||
/>
|
||||
}
|
||||
id="teamSettings"
|
||||
modalId="team_settings"
|
||||
show={true}
|
||||
text="Team Settings"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
<Connect(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"teamID": "someteamid",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-user-plus"
|
||||
/>
|
||||
}
|
||||
id="manageGroups"
|
||||
modalId="manage_team_groups"
|
||||
show={true}
|
||||
text="Manage Groups"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
<Connect(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"remove_user_from_team",
|
||||
"manage_team_roles",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-users"
|
||||
/>
|
||||
}
|
||||
id="manageMembers"
|
||||
modalId="team_members"
|
||||
show={true}
|
||||
text="Manage Members"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
<Connect(TeamPermissionGate)
|
||||
invert={true}
|
||||
permissions={
|
||||
Array [
|
||||
"remove_user_from_team",
|
||||
"manage_team_roles",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-users"
|
||||
/>
|
||||
}
|
||||
id="viewMembers"
|
||||
modalId="team_members"
|
||||
show={true}
|
||||
text="View Members"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<Connect(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"create_team",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuItemLink
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-plus-square"
|
||||
/>
|
||||
}
|
||||
id="createTeam"
|
||||
show={true}
|
||||
text="Create a Team"
|
||||
to="/create_team"
|
||||
/>
|
||||
</Connect(SystemPermissionGate)>
|
||||
<MenuItemLink
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-plus-square"
|
||||
/>
|
||||
}
|
||||
id="joinTeam"
|
||||
show={true}
|
||||
text="Join Another Team"
|
||||
to="/select_team"
|
||||
/>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={<LeaveTeamIcon />}
|
||||
id="leaveTeam"
|
||||
modalId="leave_team"
|
||||
show={false}
|
||||
text="Leave Team"
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItemAction
|
||||
id="someplugin_pluginmenuitem"
|
||||
key="someplugin_pluginmenuitem"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="some plugin text"
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItemLink
|
||||
id="integrations"
|
||||
show={false}
|
||||
text="Integrations"
|
||||
to="/somename/integrations"
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItemExternalLink
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-question"
|
||||
/>
|
||||
}
|
||||
id="helpLink"
|
||||
show={false}
|
||||
text="Help"
|
||||
/>
|
||||
<MenuItemExternalLink
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-phone"
|
||||
/>
|
||||
}
|
||||
id="reportLink"
|
||||
show={false}
|
||||
text="Report a Problem"
|
||||
/>
|
||||
<MenuItemExternalLink
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-mobile"
|
||||
/>
|
||||
}
|
||||
id="nativeAppLink"
|
||||
show={true}
|
||||
text="Download Apps"
|
||||
url=""
|
||||
/>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-info"
|
||||
/>
|
||||
}
|
||||
id="about"
|
||||
modalId="about"
|
||||
show={true}
|
||||
text="About Mattermost"
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItemAction
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-sign-out"
|
||||
/>
|
||||
}
|
||||
id="logout"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Log Out"
|
||||
/>
|
||||
</MenuGroup>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`plugins/MainMenuActions should match snapshot in web view 1`] = `
|
||||
<div
|
||||
aria-label="team menu"
|
||||
className="a11y__popup Menu"
|
||||
role="menu"
|
||||
>
|
||||
<ul
|
||||
className="Menu__content dropdown-menu"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
<MenuGroup>
|
||||
<Connect(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="addGroupsToTeam"
|
||||
modalId="add_groups_to_team"
|
||||
show={true}
|
||||
text="Add Groups to Team"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
<Connect(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"add_user_to_team",
|
||||
"invite_guest",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
extraText="Add people to the team"
|
||||
icon={false}
|
||||
id="invitePeople"
|
||||
modalId="invitation"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Invite People"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
<Connect(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="teamSettings"
|
||||
modalId="team_settings"
|
||||
show={true}
|
||||
text="Team Settings"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
<Connect(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"teamID": "someteamid",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="manageGroups"
|
||||
modalId="manage_team_groups"
|
||||
show={true}
|
||||
text="Manage Groups"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
<Connect(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"remove_user_from_team",
|
||||
"manage_team_roles",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="manageMembers"
|
||||
modalId="team_members"
|
||||
show={true}
|
||||
text="Manage Members"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
<Connect(TeamPermissionGate)
|
||||
invert={true}
|
||||
permissions={
|
||||
Array [
|
||||
"remove_user_from_team",
|
||||
"manage_team_roles",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="viewMembers"
|
||||
modalId="team_members"
|
||||
show={true}
|
||||
text="View Members"
|
||||
/>
|
||||
</Connect(TeamPermissionGate)>
|
||||
<MenuItemLink
|
||||
id="joinTeam"
|
||||
show={true}
|
||||
text="Join Another Team"
|
||||
to="/select_team"
|
||||
/>
|
||||
<MenuItemToggleModalRedux
|
||||
className="destructive"
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="leaveTeam"
|
||||
modalId="leave_team"
|
||||
show={false}
|
||||
text="Leave Team"
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<Connect(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"create_team",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuItemLink
|
||||
className=""
|
||||
disabled={false}
|
||||
id="createTeam"
|
||||
show={true}
|
||||
sibling={false}
|
||||
text="Create a Team"
|
||||
to="/create_team"
|
||||
/>
|
||||
</Connect(SystemPermissionGate)>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItemAction
|
||||
icon={false}
|
||||
id="someplugin_pluginmenuitem"
|
||||
key="someplugin_pluginmenuitem"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="some plugin text"
|
||||
/>
|
||||
</MenuGroup>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,107 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`plugins/PostMessageView should match snapshot with extended post type 1`] = `
|
||||
<PostMessageView
|
||||
currentRelativeTeamUrl="team_url"
|
||||
currentUser={
|
||||
Object {
|
||||
"username": "username",
|
||||
}
|
||||
}
|
||||
emojis={
|
||||
Object {
|
||||
"name": "smile",
|
||||
}
|
||||
}
|
||||
enableFormatting={true}
|
||||
isRHS={false}
|
||||
options={Object {}}
|
||||
pluginPostTypes={
|
||||
Object {
|
||||
"testtype": Object {
|
||||
"component": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
post={
|
||||
Object {
|
||||
"id": "post_id",
|
||||
"message": "this is some text",
|
||||
"type": "testtype",
|
||||
}
|
||||
}
|
||||
team={
|
||||
Object {
|
||||
"name": "team_name",
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"id": "theme_id",
|
||||
}
|
||||
}
|
||||
>
|
||||
<PostTypePlugin
|
||||
isRHS={false}
|
||||
post={
|
||||
Object {
|
||||
"id": "post_id",
|
||||
"message": "this is some text",
|
||||
"type": "testtype",
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"id": "theme_id",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
id="pluginId"
|
||||
>
|
||||
PostTypePlugin
|
||||
</span>
|
||||
</PostTypePlugin>
|
||||
</PostMessageView>
|
||||
`;
|
||||
|
||||
exports[`plugins/PostMessageView should match snapshot with no extended post type 1`] = `
|
||||
<Connect(ShowMore)
|
||||
checkOverflow={0}
|
||||
text="this is some text"
|
||||
>
|
||||
<div
|
||||
aria-readonly="true"
|
||||
className="post-message__text"
|
||||
dir="auto"
|
||||
id="postMessageText_post_id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Connect(PostMarkdown)
|
||||
imageProps={
|
||||
Object {
|
||||
"onImageHeightChanged": [Function],
|
||||
"onImageLoaded": [Function],
|
||||
}
|
||||
}
|
||||
isRHS={false}
|
||||
mentionKeys={Array []}
|
||||
message="this is some text"
|
||||
options={Object {}}
|
||||
post={
|
||||
Object {
|
||||
"id": "post_id",
|
||||
"message": "this is some text",
|
||||
"type": "testtype",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Connect(Pluggable)
|
||||
onHeightChange={[Function]}
|
||||
pluggableName="PostMessageAttachment"
|
||||
postId="post_id"
|
||||
/>
|
||||
</Connect(ShowMore)>
|
||||
`;
|
||||
93
webapp/channels/src/plugins/test/main_menu_action.test.jsx
Обычный файл
93
webapp/channels/src/plugins/test/main_menu_action.test.jsx
Обычный файл
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import MainMenu from 'components/main_menu/main_menu';
|
||||
|
||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
describe('plugins/MainMenuActions', () => {
|
||||
const pluginAction = jest.fn();
|
||||
|
||||
const requiredProps = {
|
||||
teamId: 'someteamid',
|
||||
teamType: '',
|
||||
teamDisplayName: 'some name',
|
||||
teamName: 'somename',
|
||||
currentUser: {id: 'someuserid', roles: 'system_user'},
|
||||
enableCommands: true,
|
||||
enableCustomEmoji: true,
|
||||
enableIncomingWebhooks: true,
|
||||
enableOutgoingWebhooks: true,
|
||||
enableOAuthServiceProvider: true,
|
||||
canManageSystemBots: true,
|
||||
enableUserCreation: true,
|
||||
enableEmailInvitations: false,
|
||||
enablePluginMarketplace: true,
|
||||
showDropdown: true,
|
||||
onToggleDropdown: () => {}, //eslint-disable-line no-empty-function
|
||||
pluginMenuItems: [{id: 'someplugin', text: 'some plugin text', action: pluginAction}],
|
||||
canCreateOrDeleteCustomEmoji: true,
|
||||
canManageIntegrations: true,
|
||||
moreTeamsToJoin: true,
|
||||
guestAccessEnabled: true,
|
||||
teamIsGroupConstrained: true,
|
||||
teamUrl: '/team',
|
||||
location: {
|
||||
pathname: '/team',
|
||||
},
|
||||
actions: {
|
||||
openModal: jest.fn(),
|
||||
showMentions: jest.fn(),
|
||||
showFlaggedPosts: jest.fn(),
|
||||
closeRightHandSide: jest.fn(),
|
||||
closeRhsMenu: jest.fn(),
|
||||
getCloudLimits: jest.fn(),
|
||||
},
|
||||
isCloud: false,
|
||||
isStarterFree: false,
|
||||
subscription: {},
|
||||
userIsAdmin: true,
|
||||
isFirstAdmin: false,
|
||||
canInviteTeamMember: false,
|
||||
isFreeTrial: false,
|
||||
teamsLimitReached: false,
|
||||
usageDeltaTeams: -1,
|
||||
};
|
||||
|
||||
test('should match snapshot in web view', () => {
|
||||
let wrapper = shallowWithIntl(
|
||||
<MainMenu
|
||||
{...requiredProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper = wrapper.shallow();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.findWhere((node) => node.key() === 'someplugin_pluginmenuitem')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should match snapshot in mobile view with some plugin and ability to click plugin', () => {
|
||||
const props = {
|
||||
...requiredProps,
|
||||
mobile: true,
|
||||
};
|
||||
|
||||
let wrapper = shallowWithIntl(
|
||||
<MainMenu
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper = wrapper.shallow();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.findWhere((node) => node.key() === 'someplugin_pluginmenuitem')).toHaveLength(1);
|
||||
|
||||
wrapper.findWhere((node) => node.key() === 'someplugin_pluginmenuitem').simulate('click');
|
||||
expect(pluginAction).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
49
webapp/channels/src/plugins/test/post_type.test.jsx
Обычный файл
49
webapp/channels/src/plugins/test/post_type.test.jsx
Обычный файл
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow, mount} from 'enzyme';
|
||||
|
||||
import PostMessageView from 'components/post_view/post_message_view/post_message_view';
|
||||
|
||||
class PostTypePlugin extends React.PureComponent {
|
||||
render() {
|
||||
return <span id='pluginId'>{'PostTypePlugin'}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
describe('plugins/PostMessageView', () => {
|
||||
const post = {type: 'testtype', message: 'this is some text', id: 'post_id'};
|
||||
const pluginPostTypes = {
|
||||
testtype: {component: PostTypePlugin},
|
||||
};
|
||||
|
||||
const requiredProps = {
|
||||
post,
|
||||
pluginPostTypes,
|
||||
currentUser: {username: 'username'},
|
||||
team: {name: 'team_name'},
|
||||
emojis: {name: 'smile'},
|
||||
theme: {id: 'theme_id'},
|
||||
enableFormatting: true,
|
||||
currentRelativeTeamUrl: 'team_url',
|
||||
};
|
||||
|
||||
test('should match snapshot with extended post type', () => {
|
||||
const wrapper = mount(
|
||||
<PostMessageView {...requiredProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('#pluginId').text()).toBe('PostTypePlugin');
|
||||
});
|
||||
|
||||
test('should match snapshot with no extended post type', () => {
|
||||
const props = {...requiredProps, pluginPostTypes: {}};
|
||||
const wrapper = shallow(
|
||||
<PostMessageView {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
45
webapp/channels/src/plugins/textbox/index.test.tsx
Обычный файл
45
webapp/channels/src/plugins/textbox/index.test.tsx
Обычный файл
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import Textbox from 'components/textbox';
|
||||
|
||||
import PluginTextbox from '.';
|
||||
|
||||
describe('PluginTextbox', () => {
|
||||
const baseProps = {
|
||||
id: 'id',
|
||||
channelId: 'channelId',
|
||||
rootId: 'rootId',
|
||||
tabIndex: -1,
|
||||
value: '',
|
||||
onChange: jest.fn(),
|
||||
onKeyPress: jest.fn(),
|
||||
createMessage: 'This is a placeholder',
|
||||
supportsCommands: true,
|
||||
characterLimit: 10000,
|
||||
currentUserId: 'currentUserId',
|
||||
currentTeamId: 'currentTeamId',
|
||||
profilesInChannel: [],
|
||||
autocompleteGroups: null,
|
||||
actions: {
|
||||
autocompleteUsersInChannel: jest.fn(),
|
||||
autocompleteChannels: jest.fn(),
|
||||
searchAssociatedGroupsForReference: jest.fn(),
|
||||
},
|
||||
useChannelMentions: true,
|
||||
};
|
||||
|
||||
test('should rename suggestionListStyle to suggestionListPosition', () => {
|
||||
const props: React.ComponentProps<typeof PluginTextbox> = {
|
||||
...baseProps,
|
||||
suggestionListStyle: 'bottom',
|
||||
};
|
||||
const wrapper = shallow(<PluginTextbox {...props}/>);
|
||||
|
||||
expect(wrapper.find(Textbox).prop('suggestionListPosition')).toEqual('bottom');
|
||||
expect(wrapper.find(Textbox).prop('suggestionListStyle')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
28
webapp/channels/src/plugins/textbox/index.tsx
Обычный файл
28
webapp/channels/src/plugins/textbox/index.tsx
Обычный файл
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Textbox from 'components/textbox';
|
||||
import BaseTextbox from 'components/textbox/textbox';
|
||||
|
||||
type Props = Omit<React.ComponentPropsWithRef<typeof Textbox>, 'suggestionListPosition'> & {
|
||||
suggestionListStyle?: React.ComponentPropsWithRef<typeof Textbox>['suggestionListPosition'];
|
||||
}
|
||||
|
||||
const PluginTextbox = React.forwardRef((props: Props, ref?: React.Ref<BaseTextbox>) => {
|
||||
const {
|
||||
suggestionListStyle,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Textbox
|
||||
ref={ref}
|
||||
suggestionListPosition={suggestionListStyle}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default PluginTextbox;
|
||||
25
webapp/channels/src/plugins/useGetPluginsActivationState.ts
Обычный файл
25
webapp/channels/src/plugins/useGetPluginsActivationState.ts
Обычный файл
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {useProducts} from 'utils/products';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {suitePluginIds} from 'utils/constants';
|
||||
|
||||
export const useGetPluginsActivationState = () => {
|
||||
const pluginsList = useSelector((state: GlobalState) => state.plugins.plugins);
|
||||
const pluginProducts = useProducts();
|
||||
|
||||
let boardsProductEnabled = false;
|
||||
let playbooksProductEnabled = false;
|
||||
if (pluginProducts) {
|
||||
boardsProductEnabled = pluginProducts.some((product) => (product.pluginId === suitePluginIds.focalboard) || (product.pluginId === suitePluginIds.boards));
|
||||
playbooksProductEnabled = pluginProducts.some((product) => product.pluginId === suitePluginIds.playbooks);
|
||||
}
|
||||
const boardsPlugin = pluginsList.focalboard;
|
||||
const playbooksPlugin = pluginsList.playbooks;
|
||||
|
||||
return {boardsPlugin: Boolean(boardsPlugin), playbooksPlugin: Boolean(playbooksPlugin), boardsProductEnabled, playbooksProductEnabled};
|
||||
};
|
||||
Ссылка в новой задаче
Block a user