mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Experimental implementation for webapp plugins (#7185)
* Start of experimental implementation for webapp plugins * Updates to webapp plugin architecture * Update pluggable test * Remove debug code
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import ProfilePopover from 'components/profile_popover.jsx';
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import React from 'react';
|
||||
@@ -79,13 +80,15 @@ export default class AtMention extends React.PureComponent {
|
||||
placement='right'
|
||||
rootClose={true}
|
||||
overlay={
|
||||
<ProfilePopover
|
||||
user={user}
|
||||
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
|
||||
hide={this.hideProfilePopover}
|
||||
isRHS={this.props.isRHS}
|
||||
hasMention={this.props.hasMention}
|
||||
/>
|
||||
<Pluggable>
|
||||
<ProfilePopover
|
||||
user={user}
|
||||
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
|
||||
hide={this.hideProfilePopover}
|
||||
isRHS={this.props.isRHS}
|
||||
hasMention={this.props.hasMention}
|
||||
/>
|
||||
</Pluggable>
|
||||
}
|
||||
>
|
||||
<a className='mention-link'>{'@' + user.username}</a>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ProfilePopover from './profile_popover.jsx';
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -56,16 +58,18 @@ export default class ProfilePicture extends React.Component {
|
||||
placement='right'
|
||||
rootClose={true}
|
||||
overlay={
|
||||
<ProfilePopover
|
||||
user={this.props.user}
|
||||
src={this.props.src}
|
||||
status={this.props.status}
|
||||
isBusy={this.props.isBusy}
|
||||
hide={this.hideProfilePopover}
|
||||
isRHS={this.props.isRHS}
|
||||
hasMention={this.props.hasMention}
|
||||
/>
|
||||
}
|
||||
<Pluggable>
|
||||
<ProfilePopover
|
||||
user={this.props.user}
|
||||
src={this.props.src}
|
||||
status={this.props.status}
|
||||
isBusy={this.props.isBusy}
|
||||
hide={this.hideProfilePopover}
|
||||
isRHS={this.props.isRHS}
|
||||
hasMention={this.props.hasMention}
|
||||
/>
|
||||
</Pluggable>
|
||||
}
|
||||
>
|
||||
<span className='status-wrapper'>
|
||||
<img
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import ProfilePopover from './profile_popover.jsx';
|
||||
import Pluggable from 'plugins/pluggable';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {OverlayTrigger} from 'react-bootstrap';
|
||||
@@ -76,15 +77,17 @@ export default class UserProfile extends React.Component {
|
||||
placement='right'
|
||||
rootClose={true}
|
||||
overlay={
|
||||
<ProfilePopover
|
||||
user={this.props.user}
|
||||
src={profileImg}
|
||||
status={this.props.status}
|
||||
isBusy={this.props.isBusy}
|
||||
hide={this.hideProfilePopover}
|
||||
isRHS={this.props.isRHS}
|
||||
hasMention={this.props.hasMention}
|
||||
/>
|
||||
<Pluggable>
|
||||
<ProfilePopover
|
||||
user={this.props.user}
|
||||
src={profileImg}
|
||||
status={this.props.status}
|
||||
isBusy={this.props.isBusy}
|
||||
hide={this.hideProfilePopover}
|
||||
isRHS={this.props.isRHS}
|
||||
hasMention={this.props.hasMention}
|
||||
/>
|
||||
</Pluggable>
|
||||
}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"localforage": "1.5.0",
|
||||
"marked": "mattermost/marked#5194fc037b35036910c6542b04bb471fe56b27a9",
|
||||
"match-at": "0.1.0",
|
||||
"mattermost-redux": "mattermost/mattermost-redux#webapp-4.1",
|
||||
"mattermost-redux": "mattermost/mattermost-redux#master",
|
||||
"object-assign": "4.1.1",
|
||||
"pdfjs-dist": "1.9.441",
|
||||
"perfect-scrollbar": "0.7.1",
|
||||
|
||||
51
webapp/plugins/index.js
Normal file
51
webapp/plugins/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// EXPERIMENTAL - SUBJECT TO CHANGE
|
||||
|
||||
import store from 'stores/redux_store.jsx';
|
||||
import {ActionTypes} from 'utils/constants.jsx';
|
||||
import {getSiteURL} from 'utils/url.jsx';
|
||||
|
||||
window.plugins = {};
|
||||
|
||||
export function registerComponents(components) {
|
||||
store.dispatch({
|
||||
type: ActionTypes.RECEIVED_PLUGIN_COMPONENTS,
|
||||
data: components || {}
|
||||
});
|
||||
}
|
||||
|
||||
export function initializePlugins() {
|
||||
const pluginJson = window.mm_config.Plugins || '[]';
|
||||
|
||||
let pluginManifests;
|
||||
try {
|
||||
pluginManifests = JSON.parse(pluginJson);
|
||||
} catch (error) {
|
||||
console.error('Invalid plugins JSON: ' + error); //eslint-disable-line no-console
|
||||
return;
|
||||
}
|
||||
|
||||
pluginManifests.forEach((m) => {
|
||||
function onLoad() {
|
||||
// Add the plugin's js to the page
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.text = this.responseText;
|
||||
document.getElementsByTagName('head')[0].appendChild(script);
|
||||
|
||||
// Initialize the plugin
|
||||
console.log('Registering ' + m.id + ' plugin...'); //eslint-disable-line no-console
|
||||
const plugin = window.plugins[m.id];
|
||||
plugin.initialize(registerComponents, store);
|
||||
console.log('...done'); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
// Fetch the plugin's bundled js
|
||||
const xhrObj = new XMLHttpRequest();
|
||||
xhrObj.open('GET', getSiteURL() + m.bundle_path, true);
|
||||
xhrObj.addEventListener('load', onLoad);
|
||||
xhrObj.send('');
|
||||
});
|
||||
}
|
||||
17
webapp/plugins/pluggable/index.js
Normal file
17
webapp/plugins/pluggable/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2017 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 Pluggable from './pluggable.jsx';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
components: state.plugins.components,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Pluggable);
|
||||
55
webapp/plugins/pluggable/pluggable.jsx
Normal file
55
webapp/plugins/pluggable/pluggable.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// EXPERIMENTAL - SUBJECT TO CHANGE
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class Pluggable extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
||||
/*
|
||||
* Should be a single overridable React component
|
||||
*/
|
||||
children: PropTypes.element.isRequired,
|
||||
|
||||
/*
|
||||
* Components for overriding provided by plugins
|
||||
*/
|
||||
components: PropTypes.object.isRequired,
|
||||
|
||||
/*
|
||||
* Logged in user's theme
|
||||
*/
|
||||
theme: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const child = React.Children.only(this.props.children).type;
|
||||
const components = this.props.components;
|
||||
|
||||
if (child == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Include any props passed to this component or to the child component
|
||||
let props = {...this.props};
|
||||
Reflect.deleteProperty(props, 'children');
|
||||
Reflect.deleteProperty(props, 'components');
|
||||
props = {...props, ...this.props.children.props};
|
||||
|
||||
// Override the default component with any registered plugin's component
|
||||
if (components.hasOwnProperty(child.name)) {
|
||||
const PluginComponent = components[child.name];
|
||||
return (
|
||||
<PluginComponent
|
||||
{...props}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return React.cloneElement(this.props.children, {...props});
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import views from './views';
|
||||
import plugins from './plugins';
|
||||
|
||||
export default {
|
||||
views
|
||||
views,
|
||||
plugins
|
||||
};
|
||||
|
||||
22
webapp/reducers/plugins/index.js
Normal file
22
webapp/reducers/plugins/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
import {ActionTypes} from 'utils/constants.jsx';
|
||||
|
||||
function components(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ActionTypes.RECEIVED_PLUGIN_COMPONENTS: {
|
||||
if (action.data) {
|
||||
return {...action.data, ...state};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
components
|
||||
});
|
||||
@@ -14,6 +14,7 @@ import * as Websockets from 'actions/websocket_actions.jsx';
|
||||
import {loadMeAndConfig} from 'actions/user_actions.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import * as I18n from 'i18n/i18n.jsx';
|
||||
import {initializePlugins} from 'plugins';
|
||||
|
||||
// Import our styles
|
||||
import 'bootstrap-colorpicker/dist/css/bootstrap-colorpicker.css';
|
||||
@@ -90,6 +91,7 @@ function preRenderSetup(callwhendone) {
|
||||
|
||||
function afterIntl() {
|
||||
$.when(d1).done(() => {
|
||||
initializePlugins();
|
||||
I18n.doAddLocaleData();
|
||||
callwhendone();
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function configureStore(initialState) {
|
||||
autoRehydrate: {
|
||||
log: false
|
||||
},
|
||||
blacklist: ['errors', 'offline', 'requests', 'entities', 'views'],
|
||||
blacklist: ['errors', 'offline', 'requests', 'entities', 'views', 'plugins'],
|
||||
debounce: 500,
|
||||
transforms: [
|
||||
setTransformer
|
||||
|
||||
111
webapp/tests/plugins/__snapshots__/pluggable.test.jsx.snap
Normal file
111
webapp/tests/plugins/__snapshots__/pluggable.test.jsx.snap
Normal file
@@ -0,0 +1,111 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`plugins/Pluggable should match snapshot with no overridden component 1`] = `
|
||||
<IntlProvider>
|
||||
<Pluggable
|
||||
components={Object {}}
|
||||
theme={Object {}}
|
||||
>
|
||||
<ProfilePopover
|
||||
hasMention={false}
|
||||
isRHS={false}
|
||||
src="src"
|
||||
theme={Object {}}
|
||||
user={Object {}}
|
||||
>
|
||||
<Popover
|
||||
bsClass="popover"
|
||||
id="user-profile-popover"
|
||||
placement="right"
|
||||
theme={Object {}}
|
||||
title="@undefined"
|
||||
>
|
||||
<div
|
||||
className="popover right"
|
||||
id="user-profile-popover"
|
||||
role="tooltip"
|
||||
style={
|
||||
Object {
|
||||
"display": "block",
|
||||
"left": undefined,
|
||||
"top": undefined,
|
||||
}
|
||||
}
|
||||
theme={Object {}}
|
||||
>
|
||||
<div
|
||||
className="arrow"
|
||||
style={
|
||||
Object {
|
||||
"left": undefined,
|
||||
"top": undefined,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<h3
|
||||
className="popover-title"
|
||||
>
|
||||
@undefined
|
||||
</h3>
|
||||
<div
|
||||
className="popover-content"
|
||||
>
|
||||
<img
|
||||
className="user-popover__image"
|
||||
height="128"
|
||||
src="src"
|
||||
width="128"
|
||||
/>
|
||||
<div
|
||||
className="popover__row first"
|
||||
data-toggle="tooltip"
|
||||
>
|
||||
<a
|
||||
className="text-nowrap text-lowercase user-popover__email"
|
||||
href="#"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-paper-plane"
|
||||
/>
|
||||
<FormattedMessage
|
||||
defaultMessage="Send Message"
|
||||
id="user_profile.send.dm"
|
||||
values={Object {}}
|
||||
>
|
||||
<span>
|
||||
Send Message
|
||||
</span>
|
||||
</FormattedMessage>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</ProfilePopover>
|
||||
</Pluggable>
|
||||
</IntlProvider>
|
||||
`;
|
||||
|
||||
exports[`plugins/Pluggable should match snapshot with overridden component 1`] = `
|
||||
<Pluggable
|
||||
components={
|
||||
Object {
|
||||
"ProfilePopover": [Function],
|
||||
}
|
||||
}
|
||||
theme={Object {}}
|
||||
>
|
||||
<ProfilePopoverPlugin
|
||||
hasMention={false}
|
||||
isRHS={false}
|
||||
src="src"
|
||||
theme={Object {}}
|
||||
user={Object {}}
|
||||
>
|
||||
<span>
|
||||
ProfilePopoverPlugin
|
||||
</span>
|
||||
</ProfilePopoverPlugin>
|
||||
</Pluggable>
|
||||
`;
|
||||
50
webapp/tests/plugins/pluggable.test.jsx
Normal file
50
webapp/tests/plugins/pluggable.test.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {mount} from 'enzyme';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
|
||||
import Pluggable from 'plugins/pluggable/pluggable.jsx';
|
||||
import ProfilePopover from 'components/profile_popover.jsx';
|
||||
|
||||
class ProfilePopoverPlugin extends React.PureComponent {
|
||||
render() {
|
||||
return <span>{'ProfilePopoverPlugin'}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
describe('plugins/Pluggable', () => {
|
||||
test('should match snapshot with overridden component', () => {
|
||||
const wrapper = mount(
|
||||
<Pluggable
|
||||
components={{ProfilePopover: ProfilePopoverPlugin}}
|
||||
theme={{}}
|
||||
>
|
||||
<ProfilePopover
|
||||
user={{}}
|
||||
src='src'
|
||||
/>
|
||||
</Pluggable>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with no overridden component', () => {
|
||||
window.mm_config = {};
|
||||
const wrapper = mount(
|
||||
<IntlProvider>
|
||||
<Pluggable
|
||||
components={{}}
|
||||
theme={{}}
|
||||
>
|
||||
<ProfilePopover
|
||||
user={{}}
|
||||
src='src'
|
||||
/>
|
||||
</Pluggable>
|
||||
</IntlProvider>
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -193,7 +193,9 @@ export const ActionTypes = keyMirror({
|
||||
|
||||
BROWSER_CHANGE_FOCUS: null,
|
||||
|
||||
EMOJI_POSTED: null
|
||||
EMOJI_POSTED: null,
|
||||
|
||||
RECEIVED_PLUGIN_COMPONENTS: null
|
||||
});
|
||||
|
||||
export const WebrtcActionTypes = keyMirror({
|
||||
|
||||
@@ -5063,9 +5063,9 @@ math-expression-evaluator@^1.2.14:
|
||||
version "1.2.16"
|
||||
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.16.tgz#b357fa1ca9faefb8e48d10c14ef2bcb2d9f0a7c9"
|
||||
|
||||
mattermost-redux@mattermost/mattermost-redux#webapp-4.1:
|
||||
mattermost-redux@mattermost/mattermost-redux#master:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/31bb5c2f21b504c4b7cab6624e4884bd3fc9f294"
|
||||
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/c5a9c96468cb8099230c447c87f2ca630bbfb531"
|
||||
dependencies:
|
||||
deep-equal "1.0.1"
|
||||
harmony-reflect "1.5.1"
|
||||
|
||||
Reference in New Issue
Block a user