Adding PostAction plugin hook (#24102)

* Adding PostAction plugin hook

* Adding missing doc string

* WIP

* Simplifying it

* Adding support for selected text

* fixing linter errors

* Adding support for the plugin editor action in the thread view

* Fixing ci check-types

* Addressing PR review comments

* Fix linter error in CI

* Fixing tests
This commit is contained in:
Jesús Espino 2023-08-08 20:36:37 +02:00 committed by GitHub
parent 60fb112a27
commit e1c6ae7d85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 128 additions and 6 deletions

View File

@ -5,6 +5,7 @@ exports[`components/AdvancedCreateComment should match snapshot when cannot post
onSubmit={[Function]}
>
<AdvanceTextEditor
additionalControls={Array []}
applyMarkdown={[Function]}
badConnection={false}
canPost={false}
@ -77,6 +78,7 @@ exports[`components/AdvancedCreateComment should match snapshot, comment with me
onSubmit={[Function]}
>
<AdvanceTextEditor
additionalControls={Array []}
applyMarkdown={[Function]}
badConnection={false}
canPost={true}
@ -144,6 +146,7 @@ exports[`components/AdvancedCreateComment should match snapshot, emoji picker di
>
<FileLimitStickyBanner />
<AdvanceTextEditor
additionalControls={Array []}
applyMarkdown={[Function]}
badConnection={false}
canPost={true}
@ -216,6 +219,7 @@ exports[`components/AdvancedCreateComment should match snapshot, empty comment 1
onSubmit={[Function]}
>
<AdvanceTextEditor
additionalControls={Array []}
applyMarkdown={[Function]}
badConnection={false}
canPost={true}
@ -283,6 +287,7 @@ exports[`components/AdvancedCreateComment should match snapshot, non-empty messa
>
<FileLimitStickyBanner />
<AdvanceTextEditor
additionalControls={Array []}
applyMarkdown={[Function]}
badConnection={false}
canPost={true}

View File

@ -80,6 +80,7 @@ describe('components/AdvancedCreateComment', () => {
useLDAPGroupMentions: true,
useCustomGroupMentions: true,
openModal: jest.fn(),
postEditorActions: [],
};
const emptyDraft = {

View File

@ -19,6 +19,7 @@ import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
import * as GlobalActions from 'actions/global_actions';
import {PostDraft} from 'types/store/draft';
import {PluginComponent} from 'types/store/plugins';
import {ModalData} from 'types/actions';
import Constants, {AdvancedTextEditor as AdvancedTextEditorConst, Locations, ModalIdentifiers, Preferences} from 'utils/constants';
@ -193,6 +194,7 @@ type Props = {
useCustomGroupMentions: boolean;
isFormattingBarHidden: boolean;
searchAssociatedGroupsForReference: (prefix: string, teamId: string, channelId: string | undefined) => Promise<{ data: any }>;
postEditorActions: PluginComponent[];
}
type State = {
@ -1198,6 +1200,41 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
render() {
const draft = this.state.draft!;
const pluginItems = this.props.postEditorActions?.
map((item) => {
if (!item.component) {
return null;
}
const Component = item.component as any;
return (
<Component
key={item.id}
draft={draft}
getSelectedText={() => {
const input = this.textboxRef.current?.getInputBox();
return {
start: input.selectionStart,
end: input.selectionEnd,
};
}}
updateText={(message: string) => {
const draft = this.state.draft!;
const modifiedDraft = {
...draft,
message,
};
this.handleDraftChange(modifiedDraft);
this.setState({
draft: modifiedDraft,
});
}}
/>
);
});
return (
<form onSubmit={this.handleSubmit}>
{
@ -1251,6 +1288,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
getFileUploadTarget={this.getFileUploadTarget}
fileUploadRef={this.fileUploadRef}
isThreadView={this.props.isThreadView}
additionalControls={pluginItems.filter(Boolean)}
/>
</form>
);

View File

@ -87,6 +87,7 @@ function makeMapStateToProps() {
const groupsWithAllowReference = useLDAPGroupMentions || useCustomGroupMentions ? getAssociatedGroupsForReferenceByMention(state, channel.team_id, channel.id) : null;
const isFormattingBarHidden = getBool(state, Constants.Preferences.ADVANCED_TEXT_EDITOR, AdvancedTextEditor.COMMENT);
const currentTeamId = getCurrentTeamId(state);
const postEditorActions = state.plugins.components.PostEditorAction;
return {
currentTeamId,
@ -116,6 +117,7 @@ function makeMapStateToProps() {
channelMemberCountsByGroup,
useCustomGroupMentions,
canUploadFiles: canUploadFiles(config),
postEditorActions,
};
};
}

View File

@ -18,6 +18,7 @@ import {CommandArgs} from '@mattermost/types/integrations';
import {Group, GroupSource} from '@mattermost/types/groups';
import {FileInfo} from '@mattermost/types/files';
import {Emoji} from '@mattermost/types/emojis';
import {PluginComponent} from 'types/store/plugins';
import * as GlobalActions from 'actions/global_actions';
import Constants, {
@ -234,6 +235,7 @@ type Props = {
channelMemberCountsByGroup: ChannelMemberCountsByGroup;
useLDAPGroupMentions: boolean;
useCustomGroupMentions: boolean;
postEditorActions: PluginComponent[];
}
type State = {
@ -1398,7 +1400,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
this.setState({showEmojiPicker: false});
};
setMessageAndCaretPostion = (newMessage: string, newCaretPosition: number) => {
setMessageAndCaretPosition = (newMessage: string, newCaretPosition: number) => {
const textbox = this.textboxRef.current?.getInputBox();
this.setState({
@ -1417,7 +1419,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
};
prefillMessage = (message: string, shouldFocus?: boolean) => {
this.setMessageAndCaretPostion(message, message.length);
this.setMessageAndCaretPosition(message, message.length);
if (shouldFocus) {
const inputBox = this.textboxRef.current?.getInputBox();
@ -1439,7 +1441,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
if (this.state.message === '') {
const newMessage = ':' + emojiAlias + ': ';
this.setMessageAndCaretPostion(newMessage, newMessage.length);
this.setMessageAndCaretPosition(newMessage, newMessage.length);
} else {
const {message} = this.state;
const {firstPiece, lastPiece} = splitMessageBasedOnCaretPosition(this.state.caretPosition, message);
@ -1450,7 +1452,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
const newCaretPosition =
firstPiece === '' ? `:${emojiAlias}: `.length : `${firstPiece} :${emojiAlias}: `.length;
this.setMessageAndCaretPostion(newMessage, newCaretPosition);
this.setMessageAndCaretPosition(newMessage, newCaretPosition);
}
this.handleEmojiClose();
@ -1560,6 +1562,38 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
render() {
const {draft, canPost} = this.props;
const pluginItems = this.props.postEditorActions?.
map((item) => {
if (!item.component) {
return null;
}
const Component = item.component as any;
return (
<Component
key={item.id}
draft={draft}
getSelectedText={() => {
const input = this.textboxRef.current?.getInputBox();
return {
start: input.selectionStart,
end: input.selectionEnd,
};
}}
updateText={(message: string) => {
this.setState({
message,
});
this.handleDraftChange({
...this.props.draft,
message,
});
}}
/>
);
});
let centerClass = '';
if (!this.props.fullWidthTextBox) {
centerClass = 'center';
@ -1649,6 +1683,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
disabled={this.props.shouldShowPreview}
/>
),
...(pluginItems || []),
].filter(Boolean)}
/>
</form>

View File

@ -104,6 +104,7 @@ function makeMapStateToProps() {
const tourStep = isGuestUser ? OnboardingTourStepsForGuestUsers.SEND_MESSAGE : OnboardingTourSteps.SEND_MESSAGE;
const showSendTutorialTip = enableTutorial && tutorialStep === tourStep;
const isFormattingBarHidden = getBool(state, Preferences.ADVANCED_TEXT_EDITOR, AdvancedTextEditor.POST);
const postEditorActions = state.plugins.components.PostEditorAction;
return {
currentTeamId,
@ -143,6 +144,7 @@ function makeMapStateToProps() {
isLDAPEnabled,
useCustomGroupMentions,
isPostPriorityEnabled: isPostPriorityEnabled(state),
postEditorActions,
};
};
}

View File

@ -114,7 +114,6 @@ function makeMapStateToProps() {
postEditTimeLimit: config.PostEditTimeLimit,
isLicensed: license.IsLicensed === 'true',
teamId: getCurrentTeamId(state),
pluginMenuItems: state.plugins.components.PostDropdownMenu,
canEdit: PostUtils.canEditPost(state, post, license, config, channel, userId),
canDelete: PostUtils.canDeletePost(state, post, channel),
teamUrl,

View File

@ -213,6 +213,7 @@ function makeMapStateToProps() {
isCardOpen: selectedCard && selectedCard.id === post.id,
shouldShowDotMenu: shouldShowDotMenu(state, post, channel),
canDelete: canDeletePost(state, post, channel),
pluginActions: state.plugins.components.PostAction,
};
};
}

View File

@ -38,6 +38,7 @@ describe('PostComponent', () => {
recentEmojis: [],
replyCount: 0,
team: currentTeam,
pluginActions: [],
actions: {
markPostAsUnread: jest.fn(),
emitShortcutReactToLastPostFrom: jest.fn(),

View File

@ -14,7 +14,7 @@ import Constants, {A11yCustomEventTypes, A11yFocusEventDetail, AppEvents, Locati
import * as PostUtils from 'utils/post_utils';
import {PostPluginComponent} from 'types/store/plugins';
import {PostPluginComponent, PluginComponent} from 'types/store/plugins';
import FileAttachmentListContainer from 'components/file_attachment_list';
import DateSeparator from 'components/post_view/date_separator';
@ -118,6 +118,7 @@ export type Props = {
isPostPriorityEnabled: boolean;
isCardOpen?: boolean;
canDelete?: boolean;
pluginActions: PluginComponent[];
};
const PostComponent = (props: Props): JSX.Element => {

View File

@ -16,6 +16,7 @@ import PostFlagIcon from 'components/post_view/post_flag_icon';
import PostRecentReactions from 'components/post_view/post_recent_reactions';
import PostReaction from 'components/post_view/post_reaction';
import CommentIcon from 'components/common/comment_icon';
import {PluginComponent} from 'types/store/plugins';
import {Emoji} from '@mattermost/types/emojis';
import {Post} from '@mattermost/types/posts';
@ -49,6 +50,7 @@ type Props = {
isPostHeaderVisible?: boolean | null;
isPostBeingEdited?: boolean;
canDelete?: boolean;
pluginActions: PluginComponent[];
actions: {
emitShortcutReactToLastPostFrom: (emittedFrom: 'CENTER' | 'RHS_ROOT' | 'NO_WHERE') => void;
};
@ -175,6 +177,24 @@ const PostOptions = (props: Props): JSX.Element => {
isMenuOpen={showActionsMenu}
/>
);
let pluginItems: ReactNode = null;
if ((!isEphemeral && !post.failed && !systemMessage) && hoverLocal) {
pluginItems = props.pluginActions?.
map((item) => {
if (item.component) {
const Component = item.component as any;
return (
<Component
post={props.post}
key={item.id}
/>
);
}
return null;
}) || [];
}
const dotMenu = (
<DotMenu
post={props.post}
@ -243,6 +263,7 @@ const PostOptions = (props: Props): JSX.Element => {
{showRecentReactions}
{postReaction}
{flagIcon}
{pluginItems}
{actionsMenu}
{commentIcon}
{(collapsedThreadsEnabled || showRecentlyUsedReactions) && dotMenu}

View File

@ -496,6 +496,18 @@ export default class PluginRegistry {
return id;
});
// Register a component to the add to the post message menu shown on hover.
// Accepts a React component. Returns a unique identifier.
registerPostActionComponent = reArg(['component'], ({component}: DPluginComponentProp) => {
return dispatchPluginComponentAction('PostAction', this.id, component);
});
// Register a component to the add to the post text editor menu.
// Accepts a React component. Returns a unique identifier.
registerPostEditorActionComponent = reArg(['component'], ({component}: DPluginComponentProp) => {
return dispatchPluginComponentAction('PostEditorAction', this.id, component);
});
// Register a post menu list item by providing some text and an action function.
// Accepts the following:
// - text - A string or React element to display in the menu

View File

@ -181,6 +181,8 @@ const initialComponents: PluginsState['components'] = {
ChannelHeaderButton: [],
MobileChannelHeaderButton: [],
PostDropdownMenu: [],
PostAction: [],
PostEditorAction: [],
Product: [],
RightHandSidebarComponent: [],
UserGuideDropdownItem: [],

View File

@ -27,6 +27,8 @@ export type PluginsState = {
Product: ProductComponent[];
CallButton: PluginComponent[];
PostDropdownMenu: PluginComponent[];
PostAction: PluginComponent[];
PostEditorAction: PluginComponent[];
FilePreview: PluginComponent[];
MainMenu: PluginComponent[];
LinkTooltip: PluginComponent[];