summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorJoram Wilander <jwawilander@gmail.com>2017-08-29 09:54:02 -0400
committerGitHub <noreply@github.com>2017-08-29 09:54:02 -0400
commit257edc9ea3b25328aa44098e963815c3c3d25312 (patch)
treeed72b2f646ea9287fdccb5076b99b01bc8585a1d /webapp
parent82a8bd99cc5fe59fe4577c9b0d2c06a82c89e628 (diff)
downloadchat-257edc9ea3b25328aa44098e963815c3c3d25312.tar.gz
chat-257edc9ea3b25328aa44098e963815c3c3d25312.tar.bz2
chat-257edc9ea3b25328aa44098e963815c3c3d25312.zip
Experimental implementation for webapp plugins (#7185)
* Start of experimental implementation for webapp plugins * Updates to webapp plugin architecture * Update pluggable test * Remove debug code
Diffstat (limited to 'webapp')
-rw-r--r--webapp/components/at_mention/at_mention.jsx17
-rw-r--r--webapp/components/profile_picture.jsx24
-rw-r--r--webapp/components/user_profile.jsx21
-rw-r--r--webapp/package.json2
-rw-r--r--webapp/plugins/index.js51
-rw-r--r--webapp/plugins/pluggable/index.js17
-rw-r--r--webapp/plugins/pluggable/pluggable.jsx55
-rw-r--r--webapp/reducers/index.js4
-rw-r--r--webapp/reducers/plugins/index.js22
-rw-r--r--webapp/root.jsx2
-rw-r--r--webapp/store/index.js2
-rw-r--r--webapp/tests/plugins/__snapshots__/pluggable.test.jsx.snap111
-rw-r--r--webapp/tests/plugins/pluggable.test.jsx50
-rw-r--r--webapp/utils/constants.jsx4
-rw-r--r--webapp/yarn.lock4
15 files changed, 354 insertions, 32 deletions
diff --git a/webapp/components/at_mention/at_mention.jsx b/webapp/components/at_mention/at_mention.jsx
index 9bb2d2aad..668222cc2 100644
--- a/webapp/components/at_mention/at_mention.jsx
+++ b/webapp/components/at_mention/at_mention.jsx
@@ -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>
diff --git a/webapp/components/profile_picture.jsx b/webapp/components/profile_picture.jsx
index fbaa46127..90cea9d34 100644
--- a/webapp/components/profile_picture.jsx
+++ b/webapp/components/profile_picture.jsx
@@ -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
diff --git a/webapp/components/user_profile.jsx b/webapp/components/user_profile.jsx
index d4d900e6a..28b1e5bfb 100644
--- a/webapp/components/user_profile.jsx
+++ b/webapp/components/user_profile.jsx
@@ -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
diff --git a/webapp/package.json b/webapp/package.json
index 8691f6abf..79731d1b4 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -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",
diff --git a/webapp/plugins/index.js b/webapp/plugins/index.js
new file mode 100644
index 000000000..2e8240cec
--- /dev/null
+++ b/webapp/plugins/index.js
@@ -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('');
+ });
+}
diff --git a/webapp/plugins/pluggable/index.js b/webapp/plugins/pluggable/index.js
new file mode 100644
index 000000000..d00f18a5d
--- /dev/null
+++ b/webapp/plugins/pluggable/index.js
@@ -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);
diff --git a/webapp/plugins/pluggable/pluggable.jsx b/webapp/plugins/pluggable/pluggable.jsx
new file mode 100644
index 000000000..566e024e5
--- /dev/null
+++ b/webapp/plugins/pluggable/pluggable.jsx
@@ -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});
+ }
+}
diff --git a/webapp/reducers/index.js b/webapp/reducers/index.js
index ff2eb0d50..eb245d851 100644
--- a/webapp/reducers/index.js
+++ b/webapp/reducers/index.js
@@ -2,7 +2,9 @@
// See License.txt for license information.
import views from './views';
+import plugins from './plugins';
export default {
- views
+ views,
+ plugins
};
diff --git a/webapp/reducers/plugins/index.js b/webapp/reducers/plugins/index.js
new file mode 100644
index 000000000..9cad72715
--- /dev/null
+++ b/webapp/reducers/plugins/index.js
@@ -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
+});
diff --git a/webapp/root.jsx b/webapp/root.jsx
index 635c8e93b..65d4cb0e4 100644
--- a/webapp/root.jsx
+++ b/webapp/root.jsx
@@ -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();
});
diff --git a/webapp/store/index.js b/webapp/store/index.js
index 2b8a4fb28..2da472881 100644
--- a/webapp/store/index.js
+++ b/webapp/store/index.js
@@ -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
diff --git a/webapp/tests/plugins/__snapshots__/pluggable.test.jsx.snap b/webapp/tests/plugins/__snapshots__/pluggable.test.jsx.snap
new file mode 100644
index 000000000..2f7a5e232
--- /dev/null
+++ b/webapp/tests/plugins/__snapshots__/pluggable.test.jsx.snap
@@ -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>
+`;
diff --git a/webapp/tests/plugins/pluggable.test.jsx b/webapp/tests/plugins/pluggable.test.jsx
new file mode 100644
index 000000000..96dedb037
--- /dev/null
+++ b/webapp/tests/plugins/pluggable.test.jsx
@@ -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();
+ });
+});
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index ea6d1dba3..858ea6bbf 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -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({
diff --git a/webapp/yarn.lock b/webapp/yarn.lock
index b3d9ba14b..b376d57d6 100644
--- a/webapp/yarn.lock
+++ b/webapp/yarn.lock
@@ -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"