Skip to content

Commit

Permalink
Experimental implementation for webapp plugins (mattermost#7185)
Browse files Browse the repository at this point in the history
* Start of experimental implementation for webapp plugins

* Updates to webapp plugin architecture

* Update pluggable test

* Remove debug code
  • Loading branch information
jwilander authored Aug 29, 2017
1 parent 82a8bd9 commit 257edc9
Show file tree
Hide file tree
Showing 15 changed files with 354 additions and 32 deletions.
17 changes: 10 additions & 7 deletions webapp/components/at_mention/at_mention.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>
Expand Down
24 changes: 14 additions & 10 deletions webapp/components/profile_picture.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand Down
21 changes: 12 additions & 9 deletions webapp/components/user_profile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
51 changes: 51 additions & 0 deletions webapp/plugins/index.js
Original file line number Diff line number Diff line change
@@ -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 changes: 17 additions & 0 deletions webapp/plugins/pluggable/index.js
Original file line number Diff line number Diff line change
@@ -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 changes: 55 additions & 0 deletions webapp/plugins/pluggable/pluggable.jsx
Original file line number Diff line number Diff line change
@@ -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});
}
}
4 changes: 3 additions & 1 deletion webapp/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// See License.txt for license information.

import views from './views';
import plugins from './plugins';

export default {
views
views,
plugins
};
22 changes: 22 additions & 0 deletions webapp/reducers/plugins/index.js
Original file line number Diff line number Diff line change
@@ -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
});
2 changes: 2 additions & 0 deletions webapp/root.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -90,6 +91,7 @@ function preRenderSetup(callwhendone) {

function afterIntl() {
$.when(d1).done(() => {
initializePlugins();
I18n.doAddLocaleData();
callwhendone();
});
Expand Down
2 changes: 1 addition & 1 deletion webapp/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 257edc9

Please sign in to comment.