diff --git a/actions/views/rhs.js b/actions/views/rhs.js index e9b5960f6753..84b38cc293de 100644 --- a/actions/views/rhs.js +++ b/actions/views/rhs.js @@ -108,6 +108,16 @@ export function showSearchResults() { }; } +export function showRHSPlugin(pluginId) { + const action = { + type: ActionTypes.UPDATE_RHS_STATE, + state: RHSStates.PLUGIN, + pluginId, + }; + + return action; +} + export function showFlaggedPosts() { return async (dispatch, getState) => { const state = getState(); diff --git a/components/search_results/search_results.jsx b/components/search_results/search_results.jsx index 7ce27791a0dc..95b664f4328a 100644 --- a/components/search_results/search_results.jsx +++ b/components/search_results/search_results.jsx @@ -5,6 +5,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import Scrollbars from 'react-custom-scrollbars'; +import {FormattedMessage} from 'react-intl'; + import {debounce} from 'mattermost-redux/actions/helpers'; import * as Utils from 'utils/utils.jsx'; @@ -58,6 +60,7 @@ export default class SearchResults extends React.PureComponent { isMentionSearch: PropTypes.bool, isFlaggedPosts: PropTypes.bool, isPinnedPosts: PropTypes.bool, + isCard: PropTypes.bool, channelDisplayName: PropTypes.string.isRequired, dataRetentionEnableMessageDeletion: PropTypes.bool.isRequired, dataRetentionMessageRetentionDays: PropTypes.string, @@ -209,18 +212,51 @@ export default class SearchResults extends React.PureComponent { } } - return ( -
- + ); + + if (this.props.isMentionSearch) { + formattedTitle = ( + + ); + } else if (this.props.isFlaggedPosts) { + formattedTitle = ( + + ); + } else if (this.props.isPinnedPosts) { + formattedTitle = ( + + ); + } else if (this.props.isCard) { + formattedTitle = ( + + ); + } + + return ( +
+ + {formattedTitle} + - ); - const closeSidebarTooltip = ( ); - if (this.props.isMentionSearch) { - title = ( - - ); - } else if (this.props.isFlaggedPosts) { - title = ( - - ); - } else if (this.props.isPinnedPosts) { - title = ( - - ); - } else if (this.props.isCard) { - title = ( - - ); - } - return (
- {title} + {this.props.children}
); + } else if (isPluginView) { + content = ( +
+
{searchForm}
+ +
+ ); } else if (postCardVisible) { content = (
diff --git a/plugins/pluggable/pluggable.jsx b/plugins/pluggable/pluggable.jsx index 7ef8001b5019..ab2bc20f7464 100644 --- a/plugins/pluggable/pluggable.jsx +++ b/plugins/pluggable/pluggable.jsx @@ -26,6 +26,11 @@ export default class Pluggable extends React.PureComponent { * Logged in user's theme */ theme: PropTypes.object.isRequired, + + /* + * Id of the specific component to be plugged. + */ + pluggableId: PropTypes.string, } render() { @@ -50,8 +55,15 @@ export default class Pluggable extends React.PureComponent { props = {...props, ...childrenProps}; // Override the default component with any registered plugin's component + // Select a specific component by pluginId if available if (components.hasOwnProperty(componentName)) { - const pluginComponents = components[componentName]; + let pluginComponents = components[componentName]; + + if (this.props.pluggableId) { + pluginComponents = pluginComponents.filter( + (element) => element.id === this.props.pluggableId); + } + const content = pluginComponents.map((p) => { const PluginComponent = p.component; return ( diff --git a/plugins/registry.js b/plugins/registry.js index e556fe6ca921..007e382c1de6 100644 --- a/plugins/registry.js +++ b/plugins/registry.js @@ -11,6 +11,8 @@ import { unregisterPluginReconnectHandler, } from 'actions/websocket_actions.jsx'; +import {showRHSPlugin} from 'actions/views/rhs'; + import { registerPluginTranslationsSource, } from 'actions/views/root'; @@ -434,4 +436,28 @@ export default class PluginRegistry { registerTranslations(getTranslationsForLocale) { registerPluginTranslationsSource(this.id, getTranslationsForLocale); } + + // Register a Right-Hand Sidebar component by providing a title for the right hand component. + // Accepts the following: + // - title - A string or JSX element to display as a title for the RHS. + // - component - A react component to display in the Right-Hand Sidebar. + // Returns: + // - id: a unique identifier + // - showRHSPlugin: the action to dispatch that will open the RHS. + registerRightHandSidebarComponent(component, title) { + const id = generateId(); + + store.dispatch({ + type: ActionTypes.RECEIVED_PLUGIN_COMPONENT, + name: 'RightHandSidebarComponent', + data: { + id, + pluginId: this.id, + component, + title, + }, + }); + + return {id, showRHSPlugin: showRHSPlugin(id)}; + } } diff --git a/plugins/rhs_plugin/index.js b/plugins/rhs_plugin/index.js new file mode 100644 index 000000000000..5eca0e434e30 --- /dev/null +++ b/plugins/rhs_plugin/index.js @@ -0,0 +1,23 @@ + +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {connect} from 'react-redux'; + +import {getPluginId} from 'selectors/rhs'; + +import RHSPlugin from './rhs_plugin.jsx'; + +function mapStateToProps(state) { + const rhsPlugins = state.plugins.components.RightHandSidebarComponent; + const pluginId = getPluginId(state); + + const pluginName = rhsPlugins.find((element) => element.id === pluginId).title; + + return { + title: pluginName, + pluggableId: pluginId, + }; +} + +export default connect(mapStateToProps)(RHSPlugin); diff --git a/plugins/rhs_plugin/rhs_plugin.jsx b/plugins/rhs_plugin/rhs_plugin.jsx new file mode 100644 index 000000000000..2cfbeef954cc --- /dev/null +++ b/plugins/rhs_plugin/rhs_plugin.jsx @@ -0,0 +1,37 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import PropTypes from 'prop-types'; +import React from 'react'; + +import SearchResultsHeader from 'components/search_results_header'; + +import Pluggable from 'plugins/pluggable'; + +export default class RhsPlugin extends React.PureComponent { + static propTypes = { + title: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + ]), + pluggableId: PropTypes.string.isRequired, + } + + render() { + return ( +
+ + {this.props.title} + + +
+ ); + } +} diff --git a/reducers/views/rhs.js b/reducers/views/rhs.js index 54986d528e19..90fcc2c0e71b 100644 --- a/reducers/views/rhs.js +++ b/reducers/views/rhs.js @@ -101,6 +101,18 @@ function searchTerms(state = '', action) { } } +function pluginId(state = '', action) { + switch (action.type) { + case ActionTypes.UPDATE_RHS_STATE: + if (action.state === RHSStates.PLUGIN) { + return action.pluginId; + } + return ''; + default: + return state; + } +} + function searchResultsTerms(state = '', action) { switch (action.type) { case ActionTypes.UPDATE_RHS_SEARCH_RESULTS_TERMS: @@ -211,6 +223,7 @@ export default combineReducers({ rhsState, searchTerms, searchResultsTerms, + pluginId, isSearchingFlaggedPost, isSearchingPinnedPost, isSidebarOpen, diff --git a/reducers/views/rhs.test.js b/reducers/views/rhs.test.js index fae9ae5256f6..4ad7246e9af1 100644 --- a/reducers/views/rhs.test.js +++ b/reducers/views/rhs.test.js @@ -15,11 +15,13 @@ describe('Reducers.RHS', () => { rhsState: null, searchTerms: '', searchResultsTerms: '', + pluginId: '', isSearchingFlaggedPost: false, isSearchingPinnedPost: false, isMenuOpen: false, isSidebarOpen: false, isSidebarExpanded: false, + }; test('Initial state', () => { @@ -49,6 +51,24 @@ describe('Reducers.RHS', () => { }); }); + test('should match RHS state to plugin id', () => { + const nextState = rhsReducer( + {}, + { + type: ActionTypes.UPDATE_RHS_STATE, + state: RHSStates.PLUGIN, + pluginId: '123', + } + ); + + expect(nextState).toEqual({ + ...initialState, + pluginId: '123', + rhsState: RHSStates.PLUGIN, + isSidebarOpen: true, + }); + }); + test(`should wipe selectedPostId on ${ActionTypes.UPDATE_RHS_STATE}`, () => { const nextState = rhsReducer( { diff --git a/selectors/rhs.jsx b/selectors/rhs.jsx index 83270de56e0c..bc91bab7b892 100644 --- a/selectors/rhs.jsx +++ b/selectors/rhs.jsx @@ -24,6 +24,10 @@ export function getSelectedChannelId(state) { return state.views.rhs.selectedChannelId; } +export function getPluginId(state) { + return state.views.rhs.pluginId; +} + function getRealSelectedPost(state) { return state.entities.posts.posts[getSelectedPostId(state)]; } diff --git a/utils/constants.jsx b/utils/constants.jsx index 47b15fbbb9af..248011790680 100644 --- a/utils/constants.jsx +++ b/utils/constants.jsx @@ -444,6 +444,7 @@ export const RHSStates = { SEARCH: 'search', FLAG: 'flag', PIN: 'pin', + PLUGIN: 'plugin', }; export const UploadStatuses = {