mirror of
https://github.com/mattermost/mattermost.git
synced 2026-11-01 09:28:09 -06:00
Let plugin sidebars add their own header buttons
registerRightHandSidebarComponent takes headerControls, rendered first among the sidebar header's buttons, before expand and close, as the thread header shows Follow. Boards uses it for Follow on a card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7d2ee5e1d6
commit
1cc38cf296
7 files changed
+59
-4
No files matched your search
@@ -94,6 +94,35 @@ describe('search_results_header', () => {
|
||||
expect(screen.getByLabelText('Expand Sidebar Icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows header controls first in the button group, before expand and close', () => {
|
||||
renderWithContext(
|
||||
<SearchResultsHeader
|
||||
previousRhsState={RHSStates.FLAG as RhsState}
|
||||
canGoBack={false}
|
||||
isExpanded={false}
|
||||
channelId='channel_id'
|
||||
headerControls={(
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Follow'
|
||||
/>
|
||||
)}
|
||||
actions={{
|
||||
closeRightHandSide: jest.fn(),
|
||||
toggleRhsExpanded: jest.fn(),
|
||||
goBack: jest.fn(),
|
||||
}}
|
||||
>
|
||||
{'Title'}
|
||||
</SearchResultsHeader>,
|
||||
);
|
||||
|
||||
const follow = screen.getByRole('button', {name: 'Follow'});
|
||||
expect(screen.getByText('Title')).not.toContainElement(follow);
|
||||
const buttons = Array.from(follow.parentElement!.querySelectorAll('button')).map((b) => b.getAttribute('aria-label'));
|
||||
expect(buttons).toEqual(['Follow', 'Expand Sidebar Icon', 'Close']);
|
||||
});
|
||||
|
||||
test('should render popout button when newWindowHandler exists', () => {
|
||||
const newWindowHandler = jest.fn();
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ import type {PropsFromRedux} from './index';
|
||||
export interface Props extends PropsFromRedux {
|
||||
children: React.ReactNode;
|
||||
newWindowHandler?: () => void;
|
||||
|
||||
/** Buttons of the panel's own, shown first in the button group (like the thread header's Follow). */
|
||||
headerControls?: React.ReactNode;
|
||||
}
|
||||
|
||||
function SearchResultsHeader(props: Props) {
|
||||
@@ -72,6 +75,7 @@ function SearchResultsHeader(props: Props) {
|
||||
{props.children}
|
||||
</span>
|
||||
<div className='pull-right'>
|
||||
{props.headerControls}
|
||||
{showExpand && !isPopoutWindow() && (
|
||||
<WithTooltip
|
||||
title={sidebarTooltipContent}
|
||||
|
||||
@@ -1187,6 +1187,8 @@ export default class PluginRegistry {
|
||||
* - component - A react component to display in the Right-Hand Sidebar.
|
||||
* - title - A string or JSX element to display as a title for the RHS.
|
||||
* - showPopout - Optional boolean (default: true). Set to false to hide the "Open in new window" button in the RHS header.
|
||||
* - headerControls - Optional. A React element or component rendered first among the RHS header's buttons,
|
||||
* before expand and close, as the thread header shows Follow.
|
||||
* Returns:
|
||||
* - id: a unique identifier
|
||||
* - showRHSPlugin: the action to dispatch that will open the RHS.
|
||||
@@ -1197,14 +1199,17 @@ export default class PluginRegistry {
|
||||
'component',
|
||||
'title',
|
||||
'showPopout',
|
||||
'headerControls',
|
||||
], ({
|
||||
component,
|
||||
title,
|
||||
showPopout = true,
|
||||
headerControls,
|
||||
}: {
|
||||
component: RightHandSidebarComponent['component'];
|
||||
title: ReactResolvable;
|
||||
showPopout?: boolean;
|
||||
headerControls?: ReactResolvable;
|
||||
}) => {
|
||||
const id = generateId();
|
||||
|
||||
@@ -1214,6 +1219,7 @@ export default class PluginRegistry {
|
||||
component,
|
||||
title: resolveReactElement(title),
|
||||
showPopout,
|
||||
headerControls: headerControls === undefined ? undefined : resolveReactElement(headerControls),
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -22,6 +22,7 @@ function mapStateToProps(state: GlobalState) {
|
||||
title: pluginTitle,
|
||||
pluginId,
|
||||
showPopout: pluginComponent?.showPopout ?? true,
|
||||
headerControls: pluginComponent?.headerControls,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,13 @@ import ConnectedRhsPlugin from '.';
|
||||
|
||||
jest.mock('components/search_results_header', () => ({
|
||||
__esModule: true,
|
||||
default: ({children, newWindowHandler}: {children: React.ReactNode; newWindowHandler?: () => void}) => (
|
||||
default: ({children, newWindowHandler, headerControls}: {children: React.ReactNode; newWindowHandler?: () => void; headerControls?: React.ReactNode}) => (
|
||||
<div
|
||||
data-testid='search-results-header'
|
||||
data-has-window-handler={newWindowHandler === undefined ? 'false' : 'true'}
|
||||
>
|
||||
{children}
|
||||
<div data-testid='header-controls'>{headerControls}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -108,11 +109,12 @@ describe('RhsPlugin', () => {
|
||||
describe('mapStateToProps', () => {
|
||||
const pluggableId = 'pluggable-id';
|
||||
|
||||
function stateWithRegisteredComponent(showPopout?: boolean): DeepPartial<GlobalState> {
|
||||
function stateWithRegisteredComponent(showPopout?: boolean, headerControls?: React.ReactNode): DeepPartial<GlobalState> {
|
||||
const component: Record<string, unknown> = {
|
||||
id: pluggableId,
|
||||
pluginId: 'plugin-id',
|
||||
title: 'Test Title',
|
||||
headerControls,
|
||||
};
|
||||
if (showPopout !== undefined) {
|
||||
component.showPopout = showPopout;
|
||||
@@ -137,6 +139,12 @@ describe('RhsPlugin', () => {
|
||||
expect(screen.getByTestId('search-results-header')).toHaveAttribute('data-has-window-handler', 'true');
|
||||
});
|
||||
|
||||
it('passes the registered header controls to the header', () => {
|
||||
renderWithContext(<ConnectedRhsPlugin/>, stateWithRegisteredComponent(undefined, <button type='button'>{'Follow'}</button>));
|
||||
|
||||
expect(screen.getByTestId('header-controls')).toHaveTextContent('Follow');
|
||||
});
|
||||
|
||||
it('passes showPopout: false through to hide the popout button', () => {
|
||||
renderWithContext(<ConnectedRhsPlugin/>, stateWithRegisteredComponent(false));
|
||||
|
||||
|
||||
@@ -22,9 +22,10 @@ export type Props = {
|
||||
title: React.ReactNode;
|
||||
pluginId?: string;
|
||||
showPopout?: boolean;
|
||||
headerControls?: React.ReactNode;
|
||||
};
|
||||
|
||||
const RhsPlugin = ({showPluggable, pluggableId, title, pluginId, showPopout = true}: Props) => {
|
||||
const RhsPlugin = ({showPluggable, pluggableId, title, pluginId, showPopout = true, headerControls}: Props) => {
|
||||
const intl = useIntl();
|
||||
const currentTeam = useSelector(getCurrentTeam);
|
||||
const currentChannel = useSelector(getCurrentChannel);
|
||||
@@ -52,7 +53,10 @@ const RhsPlugin = ({showPluggable, pluggableId, title, pluginId, showPopout = tr
|
||||
id='rhsContainer'
|
||||
className='sidebar-right__body'
|
||||
>
|
||||
<SearchResultsHeader newWindowHandler={showPopout ? newWindowHandler : undefined}>
|
||||
<SearchResultsHeader
|
||||
newWindowHandler={showPopout ? newWindowHandler : undefined}
|
||||
headerControls={headerControls}
|
||||
>
|
||||
{title}
|
||||
</SearchResultsHeader>
|
||||
{
|
||||
|
||||
@@ -393,6 +393,9 @@ export type RightHandSidebarComponent = PluginComponent & {
|
||||
title: PluggableText;
|
||||
component: React.ComponentType<BasePluggableProps>;
|
||||
showPopout?: boolean;
|
||||
|
||||
/** Rendered first among the header's buttons, before expand and close (like the thread header's Follow). */
|
||||
headerControls?: React.ReactNode;
|
||||
};
|
||||
|
||||
export type SearchHintsComponent = PluginComponent & {
|
||||
|
||||
Reference in new issue
Block a user