Skip to content

Instantly share code, notes, and snippets.

@joebordes
Last active November 9, 2019 10:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joebordes/f802ddee098c2eb4a7f9663d5e39d937 to your computer and use it in GitHub Desktop.
Save joebordes/f802ddee098c2eb4a7f9663d5e39d937 to your computer and use it in GitHub Desktop.
diff --git a/actions/websocket_actions.test.jsx b/actions/websocket_actions.test.jsx
index 15925cb9e..e3cd431d2 100644
--- a/actions/websocket_actions.test.jsx
+++ b/actions/websocket_actions.test.jsx
@@ -565,7 +565,7 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {
// Pretend to be a browser, invoke onload
mockScript.onload();
- expect(initialize).toHaveBeenCalledWith(expect.anything(), store);
+ expect(initialize).toHaveBeenCalledWith(expect.anything(), store, browserHistory);
const registery = initialize.mock.calls[0][0];
const mockComponent = 'mockRootComponent';
registery.registerRootComponent(mockComponent);
@@ -617,7 +617,7 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {
// Pretend to be a browser, invoke onload
mockScript.onload();
- expect(initialize).toHaveBeenCalledWith(expect.anything(), store);
+ expect(initialize).toHaveBeenCalledWith(expect.anything(), store, browserHistory);
const registry = initialize.mock.calls[0][0];
const mockComponent = 'mockRootComponent';
registry.registerRootComponent(mockComponent);
@@ -639,7 +639,7 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {
expect(document.createElement).toHaveBeenCalledTimes(2);
mockScript.onload();
- expect(initialize).toHaveBeenCalledWith(expect.anything(), store);
+ expect(initialize).toHaveBeenCalledWith(expect.anything(), store, browserHistory);
expect(initialize).toHaveBeenCalledTimes(2);
const registry2 = initialize.mock.calls[0][0];
const mockComponent2 = 'mockRootComponent2';
diff --git a/components/channel_header/index.js b/components/channel_header/index.js
index 74d63a8a9..792eeb7a4 100644
--- a/components/channel_header/index.js
+++ b/components/channel_header/index.js
@@ -13,7 +13,7 @@ import {getCustomEmojisInText} from 'mattermost-redux/actions/emojis';
import {General} from 'mattermost-redux/constants';
import {
getCurrentChannel,
- getMyCurrentChannelMembership,
+ getMyChannelMember,
isCurrentChannelFavorite,
isCurrentChannelMuted,
isCurrentChannelReadOnly,
@@ -62,7 +62,7 @@ function makeMapStateToProps() {
return {
teamId: getCurrentTeamId(state),
channel,
- channelMember: getMyCurrentChannelMembership(state),
+ channelMember: getMyChannelMember(state, channel.id),
currentUser: user,
dmUser,
gmMembers,
diff --git a/components/channel_layout/app_route/app_route.jsx b/components/channel_layout/app_route/app_route.jsx
new file mode 100644
index 000000000..082832c9d
--- /dev/null
+++ b/components/channel_layout/app_route/app_route.jsx
@@ -0,0 +1,60 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import $ from 'jquery';
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import * as UserAgent from 'utils/user_agent.jsx';
+import Pluggable from 'plugins/pluggable';
+import ChannelHeader from 'components/channel_header';
+
+export default class AppRouter extends React.PureComponent {
+ static propTypes = {
+
+ /*
+ * Object from react-router
+ */
+ match: PropTypes.shape({
+ params: PropTypes.shape({
+ identifier: PropTypes.string.isRequired,
+ team: PropTypes.string.isRequired,
+ channel: PropTypes.string,
+ }).isRequired,
+ }).isRequired,
+ channelId: PropTypes.string,
+ actions: PropTypes.shape({
+ selectChannel: PropTypes.func.isRequired,
+ }).isRequired,
+ }
+
+ componentDidMount() {
+ if (this.props.channelId) {
+ this.props.actions.selectChannel(this.props.channelId);
+ }
+ $('body').addClass('app__body');
+
+ // IE Detection
+ if (UserAgent.isInternetExplorer() || UserAgent.isEdge()) {
+ $('body').addClass('browser--ie');
+ }
+ }
+
+ componentWillUnmount() {
+ $('body').removeClass('app__body');
+ }
+
+ render() {
+ return (
+ <div className='app__content'>
+ {this.props.channelId && <ChannelHeader channelId={this.props.channelId}/>}
+ <Pluggable
+ pluggableName={'App.' + this.props.match.params.identifier}
+ teamName={this.props.match.params.team}
+ channelName={this.props.match.params.channel}
+ />
+ </div>
+ );
+ }
+}
+
diff --git a/components/channel_layout/app_route/index.js b/components/channel_layout/app_route/index.js
new file mode 100644
index 000000000..0f665ec4b
--- /dev/null
+++ b/components/channel_layout/app_route/index.js
@@ -0,0 +1,28 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {connect} from 'react-redux';
+import {withRouter} from 'react-router-dom';
+import {bindActionCreators} from 'redux';
+
+import {getChannelByName} from 'mattermost-redux/selectors/entities/channels';
+import {selectChannel} from 'mattermost-redux/actions/channels';
+
+import AppRoute from './app_route.jsx';
+
+function mapsStateToProps(state, ownProps) {
+ const channelId = (getChannelByName(state, ownProps.match.params.channel) || {}).id;
+ return {
+ channelId,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ selectChannel,
+ }, dispatch),
+ };
+}
+
+export default withRouter(connect(mapsStateToProps, mapDispatchToProps)(AppRoute));
diff --git a/components/channel_layout/center_channel/center_channel.jsx b/components/channel_layout/center_channel/center_channel.jsx
index 889392a76..2ccca34fc 100644
--- a/components/channel_layout/center_channel/center_channel.jsx
+++ b/components/channel_layout/center_channel/center_channel.jsx
@@ -9,6 +9,7 @@ import classNames from 'classnames';
import PermalinkView from 'components/permalink_view';
import ChannelHeaderMobile from 'components/channel_header_mobile';
import ChannelIdentifierRouter from 'components/channel_layout/channel_identifier_router';
+import AppRouter from 'components/channel_layout/app_route';
export default class CenterChannel extends React.PureComponent {
static propTypes = {
@@ -61,6 +62,14 @@ export default class CenterChannel extends React.PureComponent {
/>
)}
/>
+ <Route
+ path={'/:team/apps/:identifier'}
+ component={AppRouter}
+ />
+ <Route
+ path={'/:team/channel-apps/:channel/:identifier'}
+ component={AppRouter}
+ />
<Route
path={'/:team/:path(channels|messages)/:identifier'}
component={ChannelIdentifierRouter}
diff --git a/components/sidebar/index.js b/components/sidebar/index.js
index d906e7897..220808a6c 100644
--- a/components/sidebar/index.js
+++ b/components/sidebar/index.js
@@ -67,6 +67,7 @@ function mapStateToProps(state) {
canCreatePrivateChannel,
isOpen: getIsLhsOpen(state),
unreads: getUnreads(state),
+ pluginApps: state.plugins.components.TeamApp,
};
}
diff --git a/components/sidebar/sidebar.jsx b/components/sidebar/sidebar.jsx
index 85c541c28..d50cea5ca 100644
--- a/components/sidebar/sidebar.jsx
+++ b/components/sidebar/sidebar.jsx
@@ -133,6 +133,8 @@ export default class Sidebar extends React.PureComponent {
*/
channelSwitcherOption: PropTypes.bool.isRequired,
+ pluginApps: PropTypes.array.isRequired,
+
actions: PropTypes.shape({
close: PropTypes.func.isRequired,
switchToChannelById: PropTypes.func.isRequired,
@@ -550,6 +552,7 @@ export default class Sidebar extends React.PureComponent {
const {orderedChannelIds} = this.state;
const sectionsToHide = [SidebarChannelGroups.UNREADS, SidebarChannelGroups.FAVORITE];
+ const pluginApps = this.props.pluginApps && this.props.pluginApps.filter((p) => p.show());
return (
<Scrollbars
@@ -567,6 +570,24 @@ export default class Sidebar extends React.PureComponent {
id='sidebarChannelContainer'
className='nav-pills__container'
>
+ {pluginApps && pluginApps.length > 0 &&
+ <ul
+ key='apps'
+ className='nav nav-pills nav-stacked'
+ >
+ <li>
+ <h4 id='apps'>
+ <FormattedMessage
+ id={'applications'}
+ defaultMessage={'Applications'}
+ />
+ </h4>
+ </li>
+ <Pluggable
+ pluggableName='TeamApp'
+ teamName={this.props.currentTeam.name}
+ />
+ </ul>}
{orderedChannelIds.map((sec) => {
const section = {
type: sec.type,
diff --git a/components/sidebar/sidebar.test.jsx b/components/sidebar/sidebar.test.jsx
index f040d4c28..46e9d9840 100644
--- a/components/sidebar/sidebar.test.jsx
+++ b/components/sidebar/sidebar.test.jsx
@@ -136,6 +136,7 @@ describe('component/sidebar/sidebar_channel/SidebarChannel', () => {
switchToChannelById: jest.fn(),
openModal: jest.fn(),
},
+ pluginApps: [],
redirectChannel: 'default-channel',
canCreatePublicChannel: true,
canCreatePrivateChannel: true,
diff --git a/i18n/en.json b/i18n/en.json
index 4837dad5a..f4a0e058b 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1722,6 +1722,7 @@
"app.channel.post_update_channel_purpose_message.removed": "{username} removed the channel purpose (was: {old})",
"app.channel.post_update_channel_purpose_message.updated_from": "{username} updated the channel purpose from: {old} to: {new}",
"app.channel.post_update_channel_purpose_message.updated_to": "{username} updated the channel purpose to: {new}",
+ "applications": "Applications",
"app.plugin.marketplace_plugins.app_error": "Error connecting to the marketplace server. Please check your settings in the [System Console](/admin_console/plugins/plugin_management).",
"archivedChannelMessage": "You are viewing an **archived channel**. New messages cannot be posted.",
"atmos/camo": "atmos/camo",
diff --git a/plugins/channel_header_plug/channel_header_plug.jsx b/plugins/channel_header_plug/channel_header_plug.jsx
index a1b7daeab..a68f7832a 100644
--- a/plugins/channel_header_plug/channel_header_plug.jsx
+++ b/plugins/channel_header_plug/channel_header_plug.jsx
@@ -117,9 +117,13 @@ export default class ChannelHeaderPlug extends React.PureComponent {
}
createButton = (plug) => {
+ let activeClass = '';
+ if (plug.active && plug.active()) {
+ activeClass = ' active';
+ }
return (
<HeaderIconWrapper
- buttonClass='channel-header__icon style--none'
+ buttonClass={'channel-header__icon style--none' + activeClass}
iconComponent={plug.icon}
onClick={() => plug.action(this.props.channel, this.props.channelMember)}
buttonId={plug.id}
@@ -187,7 +191,13 @@ export default class ChannelHeaderPlug extends React.PureComponent {
}
render() {
- const components = this.props.components || [];
+ let components = this.props.components || [];
+ components = components.filter((c) => {
+ if (c.show) {
+ return c.show();
+ }
+ return true;
+ });
if (components.length === 0) {
return null;
diff --git a/plugins/index.js b/plugins/index.js
index ef725f7f1..c7649d743 100644
--- a/plugins/index.js
+++ b/plugins/index.js
@@ -7,6 +7,7 @@ import {Client4} from 'mattermost-redux/client';
import store from 'stores/redux_store.jsx';
import {ActionTypes} from 'utils/constants.jsx';
+import {browserHistory} from 'utils/browser_history.jsx';
import {getSiteURL} from 'utils/url.jsx';
import PluginRegistry from 'plugins/registry';
import {unregisterAllPluginWebSocketEvents, unregisterPluginReconnectHandler} from 'actions/websocket_actions.jsx';
@@ -125,7 +126,7 @@ function initializePlugin(manifest) {
const plugin = window.plugins[manifest.id];
const registry = new PluginRegistry(manifest.id);
if (plugin && plugin.initialize) {
- plugin.initialize(registry, store);
+ plugin.initialize(registry, store, browserHistory);
}
}
diff --git a/plugins/pluggable/pluggable.jsx b/plugins/pluggable/pluggable.jsx
index ab2bc20f7..7f388d88f 100644
--- a/plugins/pluggable/pluggable.jsx
+++ b/plugins/pluggable/pluggable.jsx
@@ -65,6 +65,9 @@ export default class Pluggable extends React.PureComponent {
}
const content = pluginComponents.map((p) => {
+ if (p.show && !p.show()) {
+ return null;
+ }
const PluginComponent = p.component;
return (
<PluginComponent
diff --git a/plugins/registry.js b/plugins/registry.js
index 663254575..98fb3c056 100644
--- a/plugins/registry.js
+++ b/plugins/registry.js
@@ -78,6 +78,30 @@ export default class PluginRegistry {
return dispatchPluginComponentAction('LeftSidebarHeader', this.id, component);
}
+ // Register a in the list of apps.
+ // Accepts a React component. Returns a unique identifier.
+ registerTeamAppComponent(component, show = () => true) {
+ const id = generateId();
+ store.dispatch({
+ type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
+ name: 'TeamApp',
+ data: {
+ id,
+ pluginId: this.id,
+ component,
+ show,
+ },
+ });
+
+ return id;
+ }
+
+ // Register in the app visualization in the center panel.
+ // Accepts an id for the url and a React component. Returns a unique identifier.
+ registerAppCenterComponent(id, component) {
+ return dispatchPluginComponentAction('App.' + id, this.id, component);
+ }
+
// Register a component fixed to the bottom of the team sidebar. Does not render if
// user is only on one team and the team sidebar is not shown.
// Accepts a React component. Returns a unique identifier.
@@ -104,7 +128,7 @@ export default class PluginRegistry {
// - action - a function called when the button is clicked, passed the channel and channel member as arguments
// - dropdown_text - string or React element shown for the dropdown button description
// - tooltip_text - string shown for tooltip appear on hover
- registerChannelHeaderButtonAction(icon, action, dropdownText, tooltipText) {
+ registerChannelHeaderButtonAction(icon, action, dropdownText, tooltipText, show = () => true, active = () => false) {
const id = generateId();
const data = {
@@ -114,6 +138,8 @@ export default class PluginRegistry {
action,
dropdownText: resolveReactElement(dropdownText),
tooltipText,
+ show,
+ active,
};
store.dispatch({
diff --git a/sass/layout/_sidebar-left.scss b/sass/layout/_sidebar-left.scss
index 17c7aa7c1..e6462c36a 100644
--- a/sass/layout/_sidebar-left.scss
+++ b/sass/layout/_sidebar-left.scss
@@ -433,6 +433,10 @@
@include transition-timing-function(ease-in-out);
}
}
+ #apps {
+ margin: 1em 1em .6em 1em;
+ text-transform: uppercase;
+ }
}
.channel-loading-gif {
  • apt install docker.io docker-compose
  • apt install software-properties-common
  • adduser --system --shell /bin/bash --gecos 'mattermost development' --group --disabled-password --home /home/$USER $USER
  • usermod -aG docker $USER
  • add-apt-repository ppa:longsleep/golang-backports
  • apt-get update
  • apt-get install golang-go
  • su - $USER
  • edit .bashrc and add:
alias ll="ls -l"
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
export PATH=$PATH:/usr/local/go/bin
  • from now on you can login to this account with su - $USER and execut . .bashrc
  • mkdir -p go/src/github.com/mattermost
  • exit
  • Edit /etc/security/limits.conf as an administrator and add the following lines:
$USER  soft  nofile  8096
$USER  hard  nofile  8096
diff --git a/docker-compose.yaml b/docker-compose.yaml
index e2ad90962..0cea2921e 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,12 +1,5 @@
 version: '2.4'
 services:
-  mysql:
-    container_name: mattermost-mysql
-    ports:
-      - "3306:3306"
-    extends:
-        file: build/docker-compose.common.yml
-        service: mysql
   postgres:
     container_name: mattermost-postgres
     ports:
@@ -58,14 +51,13 @@ services:
     networks:
       - mm-test
     depends_on:
-      - mysql
       - postgres
       - minio
       - inbucket
       - openldap
       - elasticsearch
       - redis
-    command: postgres:5432 mysql:3306 minio:9000 inbucket:10080 openldap:389 elasticsearch:9200 redis:6379
+    command: postgres:5432 minio:9000 inbucket:10080 openldap:389 elasticsearch:9200 redis:6379
 
 networks:
   mm-test:
"DriverName": "postgres",
"DataSource": "postgres://mmuser:mostest@localhost:5432/mattermost_test?sslmode=disable\u0026connect_timeout=10",
  • make run-server
  • curl http://localhost:8065/api/v4/system/ping
  • yoou should see: {"AndroidLatestVersion":"","AndroidMinVersion":"","DesktopLatestVersion":"","DesktopMinVersion":"","IosLatestVersion":"","IosMinVersion":"","status":"OK"}
  • make stop-server
  • exit
  • curl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash -
  • apt-get install -y nodejs libglu1-mesa
  • su - $USER
  • . .bashrc
  • cd go/src/github.com/mattermost/mattermost-webapp/
  • npm install
  • patch -p 1 < kanban.diff
  • cd ../mattermost-server
  • make run
  • you should be able to create admin user, team and log in
  • now stop and edit the config.json file to enable uploading plugins
  • build the kanban plugin and load it
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment