Experimental implementation for webapp plugins (#7185)
* Start of experimental implementation for webapp plugins * Updates to webapp plugin architecture * Update pluggable test * Remove debug code
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
82a8bd99cc
Коммит
257edc9ea3
@@ -2,6 +2,7 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import ProfilePopover from 'components/profile_popover.jsx';
|
import ProfilePopover from 'components/profile_popover.jsx';
|
||||||
|
import Pluggable from 'plugins/pluggable';
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -79,13 +80,15 @@ export default class AtMention extends React.PureComponent {
|
|||||||
placement='right'
|
placement='right'
|
||||||
rootClose={true}
|
rootClose={true}
|
||||||
overlay={
|
overlay={
|
||||||
<ProfilePopover
|
<Pluggable>
|
||||||
user={user}
|
<ProfilePopover
|
||||||
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
|
user={user}
|
||||||
hide={this.hideProfilePopover}
|
src={Client4.getProfilePictureUrl(user.id, user.last_picture_update)}
|
||||||
isRHS={this.props.isRHS}
|
hide={this.hideProfilePopover}
|
||||||
hasMention={this.props.hasMention}
|
isRHS={this.props.isRHS}
|
||||||
/>
|
hasMention={this.props.hasMention}
|
||||||
|
/>
|
||||||
|
</Pluggable>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<a className='mention-link'>{'@' + user.username}</a>
|
<a className='mention-link'>{'@' + user.username}</a>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import ProfilePopover from './profile_popover.jsx';
|
import ProfilePopover from './profile_popover.jsx';
|
||||||
|
import Pluggable from 'plugins/pluggable';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@@ -56,16 +58,18 @@ export default class ProfilePicture extends React.Component {
|
|||||||
placement='right'
|
placement='right'
|
||||||
rootClose={true}
|
rootClose={true}
|
||||||
overlay={
|
overlay={
|
||||||
<ProfilePopover
|
<Pluggable>
|
||||||
user={this.props.user}
|
<ProfilePopover
|
||||||
src={this.props.src}
|
user={this.props.user}
|
||||||
status={this.props.status}
|
src={this.props.src}
|
||||||
isBusy={this.props.isBusy}
|
status={this.props.status}
|
||||||
hide={this.hideProfilePopover}
|
isBusy={this.props.isBusy}
|
||||||
isRHS={this.props.isRHS}
|
hide={this.hideProfilePopover}
|
||||||
hasMention={this.props.hasMention}
|
isRHS={this.props.isRHS}
|
||||||
/>
|
hasMention={this.props.hasMention}
|
||||||
}
|
/>
|
||||||
|
</Pluggable>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className='status-wrapper'>
|
<span className='status-wrapper'>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import ProfilePopover from './profile_popover.jsx';
|
import ProfilePopover from './profile_popover.jsx';
|
||||||
|
import Pluggable from 'plugins/pluggable';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
|
||||||
import {OverlayTrigger} from 'react-bootstrap';
|
import {OverlayTrigger} from 'react-bootstrap';
|
||||||
@@ -76,15 +77,17 @@ export default class UserProfile extends React.Component {
|
|||||||
placement='right'
|
placement='right'
|
||||||
rootClose={true}
|
rootClose={true}
|
||||||
overlay={
|
overlay={
|
||||||
<ProfilePopover
|
<Pluggable>
|
||||||
user={this.props.user}
|
<ProfilePopover
|
||||||
src={profileImg}
|
user={this.props.user}
|
||||||
status={this.props.status}
|
src={profileImg}
|
||||||
isBusy={this.props.isBusy}
|
status={this.props.status}
|
||||||
hide={this.hideProfilePopover}
|
isBusy={this.props.isBusy}
|
||||||
isRHS={this.props.isRHS}
|
hide={this.hideProfilePopover}
|
||||||
hasMention={this.props.hasMention}
|
isRHS={this.props.isRHS}
|
||||||
/>
|
hasMention={this.props.hasMention}
|
||||||
|
/>
|
||||||
|
</Pluggable>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"localforage": "1.5.0",
|
"localforage": "1.5.0",
|
||||||
"marked": "mattermost/marked#5194fc037b35036910c6542b04bb471fe56b27a9",
|
"marked": "mattermost/marked#5194fc037b35036910c6542b04bb471fe56b27a9",
|
||||||
"match-at": "0.1.0",
|
"match-at": "0.1.0",
|
||||||
"mattermost-redux": "mattermost/mattermost-redux#webapp-4.1",
|
"mattermost-redux": "mattermost/mattermost-redux#master",
|
||||||
"object-assign": "4.1.1",
|
"object-assign": "4.1.1",
|
||||||
"pdfjs-dist": "1.9.441",
|
"pdfjs-dist": "1.9.441",
|
||||||
"perfect-scrollbar": "0.7.1",
|
"perfect-scrollbar": "0.7.1",
|
||||||
|
|||||||
51
webapp/plugins/index.js
Обычный файл
51
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('');
|
||||||
|
});
|
||||||
|
}
|
||||||
17
webapp/plugins/pluggable/index.js
Обычный файл
17
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);
|
||||||
55
webapp/plugins/pluggable/pluggable.jsx
Обычный файл
55
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});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import views from './views';
|
import views from './views';
|
||||||
|
import plugins from './plugins';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
views
|
views,
|
||||||
|
plugins
|
||||||
};
|
};
|
||||||
|
|||||||
22
webapp/reducers/plugins/index.js
Обычный файл
22
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
|
||||||
|
});
|
||||||
@@ -14,6 +14,7 @@ import * as Websockets from 'actions/websocket_actions.jsx';
|
|||||||
import {loadMeAndConfig} from 'actions/user_actions.jsx';
|
import {loadMeAndConfig} from 'actions/user_actions.jsx';
|
||||||
import ChannelStore from 'stores/channel_store.jsx';
|
import ChannelStore from 'stores/channel_store.jsx';
|
||||||
import * as I18n from 'i18n/i18n.jsx';
|
import * as I18n from 'i18n/i18n.jsx';
|
||||||
|
import {initializePlugins} from 'plugins';
|
||||||
|
|
||||||
// Import our styles
|
// Import our styles
|
||||||
import 'bootstrap-colorpicker/dist/css/bootstrap-colorpicker.css';
|
import 'bootstrap-colorpicker/dist/css/bootstrap-colorpicker.css';
|
||||||
@@ -90,6 +91,7 @@ function preRenderSetup(callwhendone) {
|
|||||||
|
|
||||||
function afterIntl() {
|
function afterIntl() {
|
||||||
$.when(d1).done(() => {
|
$.when(d1).done(() => {
|
||||||
|
initializePlugins();
|
||||||
I18n.doAddLocaleData();
|
I18n.doAddLocaleData();
|
||||||
callwhendone();
|
callwhendone();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function configureStore(initialState) {
|
|||||||
autoRehydrate: {
|
autoRehydrate: {
|
||||||
log: false
|
log: false
|
||||||
},
|
},
|
||||||
blacklist: ['errors', 'offline', 'requests', 'entities', 'views'],
|
blacklist: ['errors', 'offline', 'requests', 'entities', 'views', 'plugins'],
|
||||||
debounce: 500,
|
debounce: 500,
|
||||||
transforms: [
|
transforms: [
|
||||||
setTransformer
|
setTransformer
|
||||||
|
|||||||
111
webapp/tests/plugins/__snapshots__/pluggable.test.jsx.snap
Обычный файл
111
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>
|
||||||
|
`;
|
||||||
50
webapp/tests/plugins/pluggable.test.jsx
Обычный файл
50
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -193,7 +193,9 @@ export const ActionTypes = keyMirror({
|
|||||||
|
|
||||||
BROWSER_CHANGE_FOCUS: null,
|
BROWSER_CHANGE_FOCUS: null,
|
||||||
|
|
||||||
EMOJI_POSTED: null
|
EMOJI_POSTED: null,
|
||||||
|
|
||||||
|
RECEIVED_PLUGIN_COMPONENTS: null
|
||||||
});
|
});
|
||||||
|
|
||||||
export const WebrtcActionTypes = keyMirror({
|
export const WebrtcActionTypes = keyMirror({
|
||||||
|
|||||||
@@ -5063,9 +5063,9 @@ math-expression-evaluator@^1.2.14:
|
|||||||
version "1.2.16"
|
version "1.2.16"
|
||||||
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.16.tgz#b357fa1ca9faefb8e48d10c14ef2bcb2d9f0a7c9"
|
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"
|
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:
|
dependencies:
|
||||||
deep-equal "1.0.1"
|
deep-equal "1.0.1"
|
||||||
harmony-reflect "1.5.1"
|
harmony-reflect "1.5.1"
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user