mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-59881 Bubble submit result and expose to plugins (#27766)
Automatic Merge
This commit is contained in:
parent
ed2d838ac7
commit
6027c850bd
@ -183,7 +183,7 @@ describe('executeCommand', () => {
|
||||
modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL,
|
||||
});
|
||||
|
||||
expect(result).toEqual({data: true});
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -198,7 +198,7 @@ describe('executeCommand', () => {
|
||||
modalId: 'user_settings',
|
||||
},
|
||||
]);
|
||||
expect(result).toEqual({data: true});
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -220,7 +220,7 @@ describe('executeCommand', () => {
|
||||
toHaveBeenCalledWith('/leave is not supported in reply threads. Use it in the center channel instead.',
|
||||
'channel_id', 'root_id');
|
||||
|
||||
expect(result).toEqual({data: true});
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
|
||||
test('should show private modal if channel is private', async () => {
|
||||
@ -236,7 +236,7 @@ describe('executeCommand', () => {
|
||||
dialogProps: {channel: {type: Constants.PRIVATE_CHANNEL}},
|
||||
});
|
||||
|
||||
expect(result).toEqual({data: true});
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
|
||||
test('should use user id as name if channel is dm', async () => {
|
||||
@ -248,7 +248,7 @@ describe('executeCommand', () => {
|
||||
const result = await store.dispatch(executeCommand('/leave', {}));
|
||||
expect(store.getActions()[0].data).toEqual([{category: 'direct_channel_show', name: 'userId', user_id: 'user123', value: 'false'}]);
|
||||
|
||||
expect(result).toEqual({data: true});
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
|
||||
test('should use channel id as name if channel is gm', async () => {
|
||||
@ -260,7 +260,7 @@ describe('executeCommand', () => {
|
||||
const result = await store.dispatch(executeCommand('/leave', {}));
|
||||
expect(store.getActions()[0].data).toEqual([{category: 'group_channel_show', name: 'channelId', user_id: 'user123', value: 'false'}]);
|
||||
|
||||
expect(result).toEqual({data: true});
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -295,7 +295,7 @@ describe('executeCommand', () => {
|
||||
type: ActionTypes.MODAL_OPEN,
|
||||
modalId: ModalIdentifiers.PLUGIN_MARKETPLACE,
|
||||
});
|
||||
expect(result).toEqual({data: true});
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
|
||||
test('should show error when marketpace is not enabled', async () => {
|
||||
@ -395,7 +395,7 @@ describe('executeCommand', () => {
|
||||
query: undefined,
|
||||
selected_field: undefined,
|
||||
}, true);
|
||||
expect(result).toEqual({data: true});
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {CommandArgs} from '@mattermost/types/integrations';
|
||||
import type {AppCallResponse} from '@mattermost/types/apps';
|
||||
import type {CommandArgs, CommandResponse} from '@mattermost/types/integrations';
|
||||
|
||||
import {IntegrationTypes} from 'mattermost-redux/action_types';
|
||||
import {unfavoriteChannel} from 'mattermost-redux/actions/channels';
|
||||
@ -39,7 +40,14 @@ import type {GlobalState} from 'types/store';
|
||||
import {doAppSubmit, openAppsModal, postEphemeralCallResponseForCommandArgs} from './apps';
|
||||
import {trackEvent} from './telemetry_actions';
|
||||
|
||||
export function executeCommand(message: string, args: CommandArgs): ActionFuncAsync<boolean, GlobalState> {
|
||||
export type ExecuteCommandReturnType = {
|
||||
frontendHandled?: boolean;
|
||||
silentFailureReason?: Error;
|
||||
commandResponse?: CommandResponse;
|
||||
appResponse?: AppCallResponse;
|
||||
}
|
||||
|
||||
export function executeCommand(message: string, args: CommandArgs): ActionFuncAsync<ExecuteCommandReturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState() as GlobalState;
|
||||
|
||||
@ -71,7 +79,7 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs
|
||||
switch (cmd) {
|
||||
case '/search':
|
||||
dispatch(PostActions.searchForTerm(msg.substring(cmdLength + 1, msg.length)));
|
||||
return {data: true};
|
||||
return {data: {frontendHandled: true}};
|
||||
case '/shortcuts':
|
||||
if (UserAgent.isMobile()) {
|
||||
const error = {message: localizeMessage('create_post.shortcutsNotSupported', 'Keyboard shortcuts are not supported on your device')};
|
||||
@ -79,20 +87,20 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs
|
||||
}
|
||||
|
||||
dispatch(openModal({modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL, dialogType: KeyboardShortcutsModal}));
|
||||
return {data: true};
|
||||
return {data: {frontendHandled: true}};
|
||||
case '/leave': {
|
||||
// /leave command not supported in reply threads.
|
||||
if (args.channel_id && args.root_id) {
|
||||
dispatch(GlobalActions.sendEphemeralPost('/leave is not supported in reply threads. Use it in the center channel instead.', args.channel_id, args.root_id));
|
||||
return {data: true};
|
||||
return {data: {frontendHandled: true}};
|
||||
}
|
||||
const channel = getCurrentChannel(state);
|
||||
if (!channel) {
|
||||
return {data: false};
|
||||
return {data: {silentFailureReason: new Error('cannot find current channel')}};
|
||||
}
|
||||
if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
dispatch(openModal({modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL, dialogType: LeaveChannelModal, dialogProps: {channel}}));
|
||||
return {data: true};
|
||||
return {data: {frontendHandled: true}};
|
||||
}
|
||||
if (
|
||||
channel.type === Constants.DM_CHANNEL ||
|
||||
@ -118,13 +126,13 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs
|
||||
dispatch(unfavoriteChannel(channel.id));
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
return {data: {frontendHandled: true}};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '/settings':
|
||||
dispatch(openModal({modalId: ModalIdentifiers.USER_SETTINGS, dialogType: UserSettingsModal, dialogProps: {isContentProductSettings: true}}));
|
||||
return {data: true};
|
||||
return {data: {frontendHandled: true}};
|
||||
case '/marketplace':
|
||||
// check if user has permissions to access the read plugins
|
||||
if (!haveICurrentTeamPermission(state, Permissions.SYSCONSOLE_WRITE_PLUGINS)) {
|
||||
@ -137,7 +145,7 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs
|
||||
}
|
||||
|
||||
dispatch(openModal({modalId: ModalIdentifiers.PLUGIN_MARKETPLACE, dialogType: MarketplaceModal, dialogProps: {openedFrom: 'command'}}));
|
||||
return {data: true};
|
||||
return {data: {frontendHandled: true}};
|
||||
case '/collapse':
|
||||
case '/expand':
|
||||
dispatch(PostActions.resetEmbedVisibility());
|
||||
@ -173,14 +181,14 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs
|
||||
if (callResp.text) {
|
||||
dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.text, args));
|
||||
}
|
||||
return {data: true};
|
||||
return {data: {appResponse: callResp}};
|
||||
case AppCallResponseTypes.FORM:
|
||||
if (callResp.form) {
|
||||
dispatch(openAppsModal(callResp.form, creq.context));
|
||||
}
|
||||
return {data: true};
|
||||
return {data: {appResponse: callResp}};
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
return {data: true};
|
||||
return {data: {appResponse: callResp}};
|
||||
default:
|
||||
return createErrorMessage(intlShim.formatMessage(
|
||||
{
|
||||
@ -213,7 +221,7 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs
|
||||
|
||||
if (msg.trim() === '/logout') {
|
||||
GlobalActions.emitUserLoggedOutEvent(hasGotoLocation ? data.goto_location : '/');
|
||||
return {data: true};
|
||||
return {data: {response: data}};
|
||||
}
|
||||
|
||||
if (data.trigger_id) {
|
||||
@ -230,6 +238,6 @@ export function executeCommand(message: string, args: CommandArgs): ActionFuncAs
|
||||
}
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
return {data: {response: data}};
|
||||
};
|
||||
}
|
||||
|
@ -358,7 +358,7 @@ describe('Actions.Posts', () => {
|
||||
|
||||
const immediateExpectedState = [{
|
||||
args: [newPost, files],
|
||||
type: 'MOCK_CREATE_POST_IMMEDIATELY',
|
||||
type: 'MOCK_CREATE_POST',
|
||||
}, {
|
||||
args: ['draft_current_channel_id', null],
|
||||
type: 'MOCK_SET_GLOBAL_ITEM',
|
||||
@ -453,8 +453,8 @@ describe('Actions.Posts', () => {
|
||||
testStore.dispatch(Actions.submitReaction('post_id_1', '+', 'emoji_name_1'));
|
||||
|
||||
expect(testStore.getActions()).toEqual([
|
||||
{args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_ADD_REACTION'},
|
||||
{args: ['emoji_name_1'], type: 'MOCK_ADD_RECENT_EMOJI'},
|
||||
{args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_ADD_REACTION'},
|
||||
]);
|
||||
});
|
||||
|
||||
@ -503,8 +503,8 @@ describe('Actions.Posts', () => {
|
||||
testStore.dispatch(Actions.toggleReaction('post_id_1', 'emoji_name_1'));
|
||||
|
||||
expect(testStore.getActions()).toEqual([
|
||||
{args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_ADD_REACTION'},
|
||||
{args: ['emoji_name_1'], type: 'MOCK_ADD_RECENT_EMOJI'},
|
||||
{args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_ADD_REACTION'},
|
||||
]);
|
||||
});
|
||||
|
||||
@ -529,8 +529,8 @@ describe('Actions.Posts', () => {
|
||||
|
||||
await testStore.dispatch(Actions.addReaction('post_id_1', 'emoji_name_1'));
|
||||
expect(testStore.getActions()).toEqual([
|
||||
{args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_ADD_REACTION'},
|
||||
{args: ['emoji_name_1'], type: 'MOCK_ADD_RECENT_EMOJI'},
|
||||
{args: ['post_id_1', 'emoji_name_1'], type: 'MOCK_ADD_REACTION'},
|
||||
]);
|
||||
});
|
||||
test('should not add reaction if we are over the limit', async () => {
|
||||
|
@ -40,12 +40,12 @@ import {
|
||||
} from 'utils/constants';
|
||||
import {matchEmoticons} from 'utils/emoticons';
|
||||
import {makeGetIsReactionAlreadyAddedToPost, makeGetUniqueEmojiNameReactionsForPost} from 'utils/post_utils';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import {completePostReceive} from './new_post';
|
||||
import type {NewPostMessageProps} from './new_post';
|
||||
import type {SubmitPostReturnType} from './views/create_comment';
|
||||
|
||||
export function handleNewPost(post: Post, msg?: {data?: NewPostMessageProps & GroupChannel}): ActionFuncAsync<boolean, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
@ -106,7 +106,7 @@ export function unflagPost(postId: string): ActionFuncAsync {
|
||||
};
|
||||
}
|
||||
|
||||
export function createPost(post: Post, files: FileInfo[]): ActionFuncAsync {
|
||||
export function createPost(post: Post, files: FileInfo[], afterSubmit?: (response: SubmitPostReturnType) => void): ActionFuncAsync<PostActions.CreatePostReturnType, GlobalState> {
|
||||
return async (dispatch) => {
|
||||
// parse message and emit emoji event
|
||||
const emojis = matchEmoticons(post.message);
|
||||
@ -115,12 +115,7 @@ export function createPost(post: Post, files: FileInfo[]): ActionFuncAsync {
|
||||
dispatch(addRecentEmojis(trimmedEmojis));
|
||||
}
|
||||
|
||||
let result;
|
||||
if (UserAgent.isIosClassic()) {
|
||||
result = await dispatch(PostActions.createPostImmediately(post, files));
|
||||
} else {
|
||||
result = await dispatch(PostActions.createPost(post, files));
|
||||
}
|
||||
const result = await dispatch(PostActions.createPost(post, files, afterSubmit));
|
||||
|
||||
if (post.root_id) {
|
||||
dispatch(storeCommentDraft(post.root_id, null));
|
||||
@ -146,23 +141,23 @@ function storeCommentDraft(rootPostId: string, draft: null): ActionFunc {
|
||||
};
|
||||
}
|
||||
|
||||
export function submitReaction(postId: string, action: string, emojiName: string): ActionFunc<unknown, GlobalState> {
|
||||
return (dispatch, getState) => {
|
||||
export function submitReaction(postId: string, action: string, emojiName: string): ActionFuncAsync<PostActions.SubmitReactionReturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState() as GlobalState;
|
||||
const getIsReactionAlreadyAddedToPost = makeGetIsReactionAlreadyAddedToPost();
|
||||
|
||||
const isReactionAlreadyAddedToPost = getIsReactionAlreadyAddedToPost(state, postId, emojiName);
|
||||
|
||||
if (action === '+' && !isReactionAlreadyAddedToPost) {
|
||||
dispatch(addReaction(postId, emojiName));
|
||||
return dispatch(addReaction(postId, emojiName));
|
||||
} else if (action === '-' && isReactionAlreadyAddedToPost) {
|
||||
dispatch(PostActions.removeReaction(postId, emojiName));
|
||||
return dispatch(PostActions.removeReaction(postId, emojiName));
|
||||
}
|
||||
return {data: true};
|
||||
return {error: new Error(`unknown action ${action}`)};
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleReaction(postId: string, emojiName: string): ActionFuncAsync<unknown, GlobalState> {
|
||||
export function toggleReaction(postId: string, emojiName: string): ActionFuncAsync<PostActions.SubmitReactionReturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const getIsReactionAlreadyAddedToPost = makeGetIsReactionAlreadyAddedToPost();
|
||||
@ -176,9 +171,9 @@ export function toggleReaction(postId: string, emojiName: string): ActionFuncAsy
|
||||
};
|
||||
}
|
||||
|
||||
export function addReaction(postId: string, emojiName: string): ActionFunc {
|
||||
export function addReaction(postId: string, emojiName: string): ActionFuncAsync<PostActions.SubmitReactionReturnType, GlobalState> {
|
||||
const getUniqueEmojiNameReactionsForPost = makeGetUniqueEmojiNameReactionsForPost();
|
||||
return (dispatch, getState) => {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState() as GlobalState;
|
||||
const config = getConfig(state);
|
||||
const uniqueEmojiNames = getUniqueEmojiNameReactionsForPost(state, postId) ?? [];
|
||||
@ -193,12 +188,12 @@ export function addReaction(postId: string, emojiName: string): ActionFunc {
|
||||
onExited: () => closeModal(ModalIdentifiers.REACTION_LIMIT_REACHED),
|
||||
},
|
||||
}));
|
||||
return {data: false};
|
||||
return {error: new Error('reached reaction limit')};
|
||||
}
|
||||
|
||||
dispatch(PostActions.addReaction(postId, emojiName));
|
||||
dispatch(addRecentEmoji(emojiName));
|
||||
return {data: true};
|
||||
const result = await dispatch(PostActions.addReaction(postId, emojiName));
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,8 @@
|
||||
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
|
||||
import {
|
||||
addMessageIntoHistory,
|
||||
} from 'mattermost-redux/actions/posts';
|
||||
import type {CreatePostReturnType, SubmitReactionReturnType} from 'mattermost-redux/actions/posts';
|
||||
import {addMessageIntoHistory} from 'mattermost-redux/actions/posts';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
@ -25,6 +24,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import type {ActionFunc, ActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||
import {isPostPendingOrFailed} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import type {ExecuteCommandReturnType} from 'actions/command';
|
||||
import {executeCommand} from 'actions/command';
|
||||
import {runMessageWillBePostedHooks, runSlashCommandWillBePostedHooks} from 'actions/hooks';
|
||||
import * as PostActions from 'actions/post_actions';
|
||||
@ -56,7 +56,7 @@ export function updateCommentDraft(rootId: string, draft?: PostDraft, save = fal
|
||||
return updateDraft(key, draft ?? null, rootId, save);
|
||||
}
|
||||
|
||||
export function submitPost(channelId: string, rootId: string, draft: PostDraft): ActionFuncAsync {
|
||||
export function submitPost(channelId: string, rootId: string, draft: PostDraft, afterSubmit?: (response: SubmitPostReturnType) => void): ActionFuncAsync<CreatePostReturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
@ -103,11 +103,13 @@ export function submitPost(channelId: string, rootId: string, draft: PostDraft):
|
||||
|
||||
post = hookResult.data;
|
||||
|
||||
return dispatch(PostActions.createPost(post, draft.fileInfos));
|
||||
return dispatch(PostActions.createPost(post, draft.fileInfos, afterSubmit));
|
||||
};
|
||||
}
|
||||
|
||||
export function submitCommand(channelId: string, rootId: string, draft: PostDraft): ActionFuncAsync<unknown, GlobalState> {
|
||||
type SubmitCommandRerturnType = ExecuteCommandReturnType & CreatePostReturnType;
|
||||
|
||||
export function submitCommand(channelId: string, rootId: string, draft: PostDraft): ActionFuncAsync<SubmitCommandRerturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
@ -126,13 +128,13 @@ export function submitCommand(channelId: string, rootId: string, draft: PostDraf
|
||||
return {error: hookResult.error};
|
||||
} else if (!hookResult.data!.message && !hookResult.data!.args) {
|
||||
// do nothing with an empty return from a hook
|
||||
return {};
|
||||
return {error: new Error('command not submitted due to plugin hook')};
|
||||
}
|
||||
|
||||
message = hookResult.data!.message;
|
||||
args = hookResult.data!.args;
|
||||
|
||||
const {error} = await dispatch(executeCommand(message, args));
|
||||
const {error, data} = await dispatch(executeCommand(message, args));
|
||||
|
||||
if (error) {
|
||||
if (error.sendMessage) {
|
||||
@ -141,7 +143,7 @@ export function submitCommand(channelId: string, rootId: string, draft: PostDraf
|
||||
throw (error);
|
||||
}
|
||||
|
||||
return {};
|
||||
return {data: data!};
|
||||
};
|
||||
}
|
||||
|
||||
@ -175,7 +177,9 @@ export function makeOnSubmit(channelId: string, rootId: string, latestPostId: st
|
||||
};
|
||||
}
|
||||
|
||||
export function onSubmit(draft: PostDraft, options: {ignoreSlash?: boolean}): ActionFuncAsync<boolean, GlobalState> {
|
||||
export type SubmitPostReturnType = CreatePostReturnType & SubmitCommandRerturnType & SubmitReactionReturnType;
|
||||
|
||||
export function onSubmit(draft: PostDraft, options: {ignoreSlash?: boolean; afterSubmit?: (response: SubmitPostReturnType) => void}): ActionFuncAsync<SubmitPostReturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const {message, channelId, rootId} = draft;
|
||||
const state = getState();
|
||||
@ -190,14 +194,16 @@ export function onSubmit(draft: PostDraft, options: {ignoreSlash?: boolean}): Ac
|
||||
if (isReaction && emojiMap.has(isReaction[2])) {
|
||||
const latestPostId = getLatestInteractablePostId(state, channelId, rootId);
|
||||
if (latestPostId) {
|
||||
dispatch(PostActions.submitReaction(latestPostId, isReaction[1], isReaction[2]));
|
||||
return dispatch(PostActions.submitReaction(latestPostId, isReaction[1], isReaction[2]));
|
||||
}
|
||||
} else if (message.indexOf('/') === 0 && !options.ignoreSlash) {
|
||||
await dispatch(submitCommand(channelId, rootId, draft));
|
||||
} else {
|
||||
await dispatch(submitPost(channelId, rootId, draft));
|
||||
return {error: new Error('no post to react to')};
|
||||
}
|
||||
return {data: true};
|
||||
|
||||
if (message.indexOf('/') === 0 && !options.ignoreSlash) {
|
||||
return dispatch(submitCommand(channelId, rootId, draft));
|
||||
}
|
||||
|
||||
return dispatch(submitPost(channelId, rootId, draft, options.afterSubmit));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {SubmitPostReturnType} from 'actions/views/create_comment';
|
||||
|
||||
import AdvancedTextEditor from 'components/advanced_text_editor/advanced_text_editor';
|
||||
|
||||
import {Locations} from 'utils/constants';
|
||||
@ -19,6 +21,11 @@ export type Props = {
|
||||
|
||||
isThreadView?: boolean;
|
||||
placeholder?: string;
|
||||
|
||||
/**
|
||||
* Used by plugins to act after the post is made
|
||||
*/
|
||||
afterSubmit?: (response: SubmitPostReturnType) => void;
|
||||
}
|
||||
|
||||
const AdvancedCreateComment = ({
|
||||
@ -26,6 +33,7 @@ const AdvancedCreateComment = ({
|
||||
rootId,
|
||||
isThreadView,
|
||||
placeholder,
|
||||
afterSubmit,
|
||||
}: Props) => {
|
||||
return (
|
||||
<AdvancedTextEditor
|
||||
@ -34,6 +42,7 @@ const AdvancedCreateComment = ({
|
||||
postId={rootId}
|
||||
isThreadView={isThreadView}
|
||||
placeholder={placeholder}
|
||||
afterSubmit={afterSubmit}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -18,6 +18,7 @@ import {getCurrentUserId, isCurrentUserGuestUser, getStatusForUserId, makeGetDis
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions';
|
||||
import {actionOnGlobalItemsWithPrefix} from 'actions/storage';
|
||||
import type {SubmitPostReturnType} from 'actions/views/create_comment';
|
||||
import {removeDraft, updateDraft} from 'actions/views/drafts';
|
||||
import {makeGetDraft} from 'selectors/rhs';
|
||||
import {connectionErrorCount} from 'selectors/views/system';
|
||||
@ -79,14 +80,20 @@ type Props = {
|
||||
postId: string;
|
||||
isThreadView?: boolean;
|
||||
placeholder?: string;
|
||||
|
||||
/**
|
||||
* Used by plugins to act after the post is made
|
||||
*/
|
||||
afterSubmit?: (response: SubmitPostReturnType) => void;
|
||||
}
|
||||
|
||||
const AdvanceTextEditor = ({
|
||||
const AdvancedTextEditor = ({
|
||||
location,
|
||||
channelId,
|
||||
postId,
|
||||
isThreadView = false,
|
||||
placeholder,
|
||||
afterSubmit,
|
||||
}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
@ -244,7 +251,7 @@ const AdvanceTextEditor = ({
|
||||
isValidPersistentNotifications,
|
||||
onSubmitCheck: prioritySubmitCheck,
|
||||
} = usePriority(draft, handleDraftChange, focusTextbox, showPreview);
|
||||
const [handleSubmit, errorClass] = useSubmit(draft, postError, channelId, postId, serverError, lastBlurAt, focusTextbox, setServerError, setPostError, setShowPreview, handleDraftChange, prioritySubmitCheck);
|
||||
const [handleSubmit, errorClass] = useSubmit(draft, postError, channelId, postId, serverError, lastBlurAt, focusTextbox, setServerError, setPostError, setShowPreview, handleDraftChange, prioritySubmitCheck, afterSubmit);
|
||||
const [handleKeyDown, postMsgKeyPress] = useKeyHandler(
|
||||
draft,
|
||||
channelId,
|
||||
@ -660,4 +667,4 @@ const AdvanceTextEditor = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvanceTextEditor;
|
||||
export default AdvancedTextEditor;
|
||||
|
@ -16,6 +16,7 @@ import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles'
|
||||
import {getCurrentUserId, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {scrollPostListToBottom} from 'actions/views/channel';
|
||||
import type {SubmitPostReturnType} from 'actions/views/create_comment';
|
||||
import {onSubmit} from 'actions/views/create_comment';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
@ -60,6 +61,7 @@ const useSubmit = (
|
||||
setShowPreview: (showPreview: boolean) => void,
|
||||
handleDraftChange: (draft: PostDraft, options?: {instant?: boolean; show?: boolean}) => void,
|
||||
prioritySubmitCheck: (onConfirm: () => void) => boolean,
|
||||
afterSubmit?: (response: SubmitPostReturnType) => void,
|
||||
): [
|
||||
(e: React.FormEvent, submittingDraft?: PostDraft) => void,
|
||||
string | null,
|
||||
@ -153,7 +155,7 @@ const useSubmit = (
|
||||
setServerError(null);
|
||||
|
||||
const ignoreSlash = isErrorInvalidSlashCommand(serverError) && serverError?.submittedMessage === submittingDraft.message;
|
||||
const options = {ignoreSlash};
|
||||
const options = {ignoreSlash, afterSubmit};
|
||||
|
||||
try {
|
||||
await dispatch(onSubmit(submittingDraft, options));
|
||||
@ -188,7 +190,7 @@ const useSubmit = (
|
||||
}
|
||||
|
||||
isDraftSubmitting.current = false;
|
||||
}, [handleDraftChange, dispatch, draft, focusTextbox, isRootDeleted, postError, serverError, showPostDeletedModal, channelId, postId, lastBlurAt, setPostError, setServerError]);
|
||||
}, [handleDraftChange, dispatch, draft, focusTextbox, isRootDeleted, postError, serverError, showPostDeletedModal, channelId, postId, lastBlurAt, setPostError, setServerError, afterSubmit]);
|
||||
|
||||
const showNotifyAllModal = useCallback((mentions: string[], channelTimezoneCount: number, memberNotifyCount: number) => {
|
||||
dispatch(openModal({
|
||||
|
@ -89,38 +89,6 @@ describe('Actions.Posts', () => {
|
||||
expect(!postsInChannel[channelId]).toBeTruthy();
|
||||
});
|
||||
|
||||
it('maintain postReplies', async () => {
|
||||
const channelId = TestHelper.basicChannel!.id;
|
||||
const post = TestHelper.fakePost(channelId);
|
||||
const postId = TestHelper.generateId();
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
post('/posts').
|
||||
reply(201, {...post, id: postId});
|
||||
|
||||
await store.dispatch(Actions.createPostImmediately(post));
|
||||
|
||||
const post2 = TestHelper.fakePostWithId(channelId);
|
||||
post2.root_id = postId;
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
post('/posts').
|
||||
reply(201, post2);
|
||||
|
||||
await store.dispatch(Actions.createPostImmediately(post2));
|
||||
|
||||
expect(store.getState().entities.posts.postsReplies[postId]).toBe(1);
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
delete(`/posts/${post2.id}`).
|
||||
reply(200, OK_RESPONSE);
|
||||
|
||||
await store.dispatch(Actions.deletePost(post2));
|
||||
await store.dispatch(Actions.removePost(post2));
|
||||
|
||||
expect(store.getState().entities.posts.postsReplies[postId]).toBe(0);
|
||||
});
|
||||
|
||||
it('resetCreatePostRequest', async () => {
|
||||
const channelId = TestHelper.basicChannel!.id;
|
||||
const post = TestHelper.fakePost(channelId);
|
||||
|
@ -8,6 +8,7 @@ import type {Channel, ChannelUnread} from '@mattermost/types/channels';
|
||||
import type {FetchPaginatedThreadOptions} from '@mattermost/types/client4';
|
||||
import type {Group} from '@mattermost/types/groups';
|
||||
import type {Post, PostList, PostAcknowledgement} from '@mattermost/types/posts';
|
||||
import type {Reaction} from '@mattermost/types/reactions';
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
@ -168,7 +169,12 @@ export function getPost(postId: string): ActionFuncAsync<Post> {
|
||||
};
|
||||
}
|
||||
|
||||
export function createPost(post: Post, files: any[] = []): ActionFuncAsync {
|
||||
export type CreatePostReturnType = {
|
||||
created?: boolean;
|
||||
pending?: string;
|
||||
}
|
||||
|
||||
export function createPost(post: Post, files: any[] = [], afterSubmit?: (response: any) => void): ActionFuncAsync<CreatePostReturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = state.entities.users.currentUserId;
|
||||
@ -178,7 +184,7 @@ export function createPost(post: Post, files: any[] = []): ActionFuncAsync {
|
||||
let actions: AnyAction[] = [];
|
||||
|
||||
if (PostSelectors.isPostIdSending(state, pendingPostId)) {
|
||||
return {data: true};
|
||||
return {data: {pending: pendingPostId}};
|
||||
}
|
||||
|
||||
let newPost = {
|
||||
@ -266,6 +272,8 @@ export function createPost(post: Post, files: any[] = []): ActionFuncAsync {
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_CREATE_POST'));
|
||||
afterSubmit?.({created});
|
||||
return {data: {created}};
|
||||
} catch (error) {
|
||||
const data = {
|
||||
...newPost,
|
||||
@ -288,107 +296,11 @@ export function createPost(post: Post, files: any[] = []): ActionFuncAsync {
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_CREATE_POST_FAILED'));
|
||||
return {error};
|
||||
}
|
||||
}());
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function createPostImmediately(post: Post, files: any[] = []): ActionFuncAsync<Post> {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = state.entities.users.currentUserId;
|
||||
const timestamp = Date.now();
|
||||
const pendingPostId = `${currentUserId}:${timestamp}`;
|
||||
|
||||
let newPost: Post = {
|
||||
...post,
|
||||
pending_post_id: pendingPostId,
|
||||
create_at: timestamp,
|
||||
update_at: timestamp,
|
||||
reply_count: 0,
|
||||
};
|
||||
|
||||
if (post.root_id) {
|
||||
newPost.reply_count = PostSelectors.getPostRepliesCount(state, post.root_id) + 1;
|
||||
}
|
||||
|
||||
if (files.length) {
|
||||
const fileIds = files.map((file) => file.id);
|
||||
|
||||
newPost = {
|
||||
...newPost,
|
||||
file_ids: fileIds,
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: FileTypes.RECEIVED_FILES_FOR_POST,
|
||||
postId: pendingPostId,
|
||||
data: files,
|
||||
});
|
||||
dispatch({
|
||||
type: ChannelTypes.INCREMENT_FILE_COUNT,
|
||||
amount: files.length,
|
||||
id: newPost.channel_id,
|
||||
});
|
||||
}
|
||||
|
||||
const crtEnabled = isCollapsedThreadsEnabled(state);
|
||||
dispatch(receivedNewPost({
|
||||
...newPost,
|
||||
id: pendingPostId,
|
||||
}, crtEnabled));
|
||||
|
||||
try {
|
||||
const created = await Client4.createPost({...newPost, create_at: 0});
|
||||
newPost.id = created.id;
|
||||
newPost.reply_count = created.reply_count;
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch({type: PostTypes.CREATE_POST_FAILURE, data: newPost, error});
|
||||
dispatch(removePost({
|
||||
...newPost,
|
||||
id: pendingPostId,
|
||||
}));
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
const actions: AnyAction[] = [
|
||||
receivedPost(newPost, crtEnabled),
|
||||
{
|
||||
type: PostTypes.CREATE_POST_SUCCESS,
|
||||
},
|
||||
{
|
||||
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
|
||||
data: {
|
||||
channelId: newPost.channel_id,
|
||||
amount: 1,
|
||||
amountRoot: newPost.root_id === '' ? 1 : 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
|
||||
data: {
|
||||
channelId: newPost.channel_id,
|
||||
amount: 1,
|
||||
amountRoot: newPost.root_id === '' ? 1 : 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (files) {
|
||||
actions.push({
|
||||
type: FileTypes.RECEIVED_FILES_FOR_POST,
|
||||
postId: newPost.id,
|
||||
data: files,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
|
||||
return {data: newPost};
|
||||
return {data: {created: true}};
|
||||
};
|
||||
}
|
||||
|
||||
@ -589,7 +501,12 @@ export function unpinPost(postId: string): ActionFuncAsync {
|
||||
};
|
||||
}
|
||||
|
||||
export function addReaction(postId: string, emojiName: string): ActionFuncAsync {
|
||||
export type SubmitReactionReturnType = {
|
||||
reaction?: Reaction;
|
||||
removedReaction?: boolean;
|
||||
}
|
||||
|
||||
export function addReaction(postId: string, emojiName: string): ActionFuncAsync<SubmitReactionReturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const currentUserId = getState().entities.users.currentUserId;
|
||||
|
||||
@ -607,11 +524,11 @@ export function addReaction(postId: string, emojiName: string): ActionFuncAsync
|
||||
data: reaction,
|
||||
});
|
||||
|
||||
return {data: true};
|
||||
return {data: {reaction}};
|
||||
};
|
||||
}
|
||||
|
||||
export function removeReaction(postId: string, emojiName: string): ActionFuncAsync {
|
||||
export function removeReaction(postId: string, emojiName: string): ActionFuncAsync<SubmitReactionReturnType, GlobalState> {
|
||||
return async (dispatch, getState) => {
|
||||
const currentUserId = getState().entities.users.currentUserId;
|
||||
|
||||
@ -628,7 +545,7 @@ export function removeReaction(postId: string, emojiName: string): ActionFuncAsy
|
||||
data: {user_id: currentUserId, post_id: postId, emoji_name: emojiName},
|
||||
});
|
||||
|
||||
return {data: true};
|
||||
return {data: {removedReaction: true}};
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import {openModal} from 'actions/views/modals';
|
||||
import {closeRightHandSide, selectPostById} from 'actions/views/rhs';
|
||||
import {getSelectedPostId, getIsRhsOpen} from 'selectors/rhs';
|
||||
|
||||
import AdvancedTextEditor from 'components/advanced_text_editor/advanced_text_editor';
|
||||
import ChannelInviteModal from 'components/channel_invite_modal';
|
||||
import ChannelMembersModal from 'components/channel_members_modal';
|
||||
import {openPricingModal} from 'components/global_header/right_controls/plan_upgrade_button';
|
||||
@ -26,7 +27,6 @@ import {formatText} from 'utils/text_formatting';
|
||||
import {useWebSocket, useWebSocketClient, WebSocketContext} from 'utils/use_websocket';
|
||||
import {imageURLForUser} from 'utils/utils';
|
||||
|
||||
import CreatePost from './exported_create_post';
|
||||
import {openInteractiveDialog} from './interactive_dialog'; // This import has intentional side effects. Do not remove without research.
|
||||
import Textbox from './textbox';
|
||||
|
||||
@ -89,8 +89,8 @@ window.Components = {
|
||||
BotBadge: BotTag,
|
||||
StartTrialFormModal,
|
||||
ThreadViewer,
|
||||
CreatePost,
|
||||
PostMessagePreview,
|
||||
AdvancedTextEditor,
|
||||
};
|
||||
|
||||
// This is a prototype of the Product API for use by internal plugins only while we transition to the proper architecture
|
||||
|
Loading…
Reference in New Issue
Block a user