MM-52902 : Pasting text or image in center message input box shifts input box up unexpectedly (#23502)

This commit is contained in:
M-ZubairAhmed 2023-05-26 17:50:07 +05:30 committed by GitHub
parent 0a897220a7
commit 0e31ecbbe8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 92 additions and 115 deletions

View File

@ -311,9 +311,8 @@ describe('components/AdvancedCreateComment', () => {
expect(wrapper.state().serverError.message).toBe(testError1); expect(wrapper.state().serverError.message).toBe(testError1);
expect(wrapper.state().draft.uploadsInProgress).toEqual([2, 3]); expect(wrapper.state().draft.uploadsInProgress).toEqual([2, 3]);
// clientId = -1
const testError2 = 'test error 2'; const testError2 = 'test error 2';
instance.handleUploadError(testError2, -1, null, props.rootId); instance.handleUploadError(testError2, '', null, props.rootId);
// should not call onUpdateCommentDraft // should not call onUpdateCommentDraft
expect(updateCommentDraftWithRootId.mock.calls.length).toBe(1); expect(updateCommentDraftWithRootId.mock.calls.length).toBe(1);

View File

@ -1116,8 +1116,8 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
} }
}; };
handleUploadError = (err: string | ServerError | null, clientId: string | number = -1, _?: string, rootId = '') => { handleUploadError = (uploadError: string | ServerError | null, clientId?: string, _?: string, rootId = '') => {
if (clientId !== -1) { if (clientId) {
const draft = {...this.draftsForPost[rootId]!}; const draft = {...this.draftsForPost[rootId]!};
const uploadsInProgress = [...draft.uploadsInProgress]; const uploadsInProgress = [...draft.uploadsInProgress];
@ -1137,16 +1137,13 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
} }
} }
let serverError = err; if (typeof uploadError === 'string') {
if (typeof serverError === 'string') { if (uploadError.length !== 0) {
serverError = new Error(serverError); this.setState({serverError: new Error(uploadError)});
}
this.setState({serverError}, () => {
if (serverError && this.props.scrollToBottom) {
this.props.scrollToBottom();
} }
}); } else {
this.setState({serverError: uploadError});
}
}; };
removePreview = (id: string) => { removePreview = (id: string) => {

View File

@ -1038,34 +1038,32 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
this.handleDraftChange(draft, true); this.handleDraftChange(draft, true);
}; };
handleUploadError = (err: string | ServerError, clientId?: string, channelId?: string) => { handleUploadError = (uploadError: string | ServerError | null, clientId?: string, channelId?: string) => {
let serverError = err; if (clientId && channelId) {
if (typeof serverError === 'string') { const draft = {...this.draftsForChannel[channelId]!};
serverError = new Error(serverError);
}
if (!channelId || !clientId) { if (draft.uploadsInProgress) {
this.setState({serverError}); const index = draft.uploadsInProgress.indexOf(clientId);
return;
}
const draft = {...this.draftsForChannel[channelId]!}; if (index !== -1) {
const uploadsInProgress = draft.uploadsInProgress.filter((item, itemIndex) => index !== itemIndex);
if (draft.uploadsInProgress) { const modifiedDraft = {
const index = draft.uploadsInProgress.indexOf(clientId); ...draft,
uploadsInProgress,
if (index !== -1) { };
const uploadsInProgress = draft.uploadsInProgress.filter((item, itemIndex) => index !== itemIndex); this.props.actions.setDraft(StoragePrefixes.DRAFT + channelId, modifiedDraft, channelId);
const modifiedDraft = { this.draftsForChannel[channelId] = modifiedDraft;
...draft, }
uploadsInProgress,
};
this.props.actions.setDraft(StoragePrefixes.DRAFT + channelId, modifiedDraft, channelId);
this.draftsForChannel[channelId] = modifiedDraft;
} }
} }
this.setState({serverError}); if (typeof uploadError === 'string') {
if (uploadError.length !== 0) {
this.setState({serverError: new Error(uploadError)});
}
} else {
this.setState({serverError: uploadError});
}
}; };
removePreview = (id: string) => { removePreview = (id: string) => {

View File

@ -88,7 +88,7 @@ type Props = {
hideEmojiPicker: () => void; hideEmojiPicker: () => void;
toggleAdvanceTextEditor: () => void; toggleAdvanceTextEditor: () => void;
handleUploadProgress: (filePreviewInfo: FilePreviewInfo) => void; handleUploadProgress: (filePreviewInfo: FilePreviewInfo) => void;
handleUploadError: (err: string | ServerError, clientId?: string, channelId?: string) => void; handleUploadError: (err: string | ServerError | null, clientId?: string, channelId?: string) => void;
handleFileUploadComplete: (fileInfos: FileInfo[], clientIds: string[], channelId: string, rootId?: string) => void; handleFileUploadComplete: (fileInfos: FileInfo[], clientIds: string[], channelId: string, rootId?: string) => void;
handleUploadStart: (clientIds: string[], channelId: string) => void; handleUploadStart: (clientIds: string[], channelId: string) => void;
handleFileUploadChange: () => void; handleFileUploadChange: () => void;
@ -199,17 +199,6 @@ const AdvanceTextEditor = ({
setKeepEditorInFocus(true); setKeepEditorInFocus(true);
}, []); }, []);
let serverErrorJsx = null;
if (serverError) {
serverErrorJsx = (
<MessageSubmitError
error={serverError}
submittedMessage={serverError.submittedMessage}
handleSubmit={handleSubmit}
/>
);
}
let attachmentPreview = null; let attachmentPreview = null;
if (!readOnlyChannel && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0)) { if (!readOnlyChannel && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0)) {
attachmentPreview = ( attachmentPreview = (
@ -551,12 +540,20 @@ const AdvanceTextEditor = ({
<div <div
id='postCreateFooter' id='postCreateFooter'
role='form' role='form'
className={classNames('AdvancedTextEditor__footer', { className={classNames('AdvancedTextEditor__footer', {'AdvancedTextEditor__footer--has-error': postError || serverError})}
'AdvancedTextEditor__footer--has-error': postError || serverError,
})}
> >
{postError && <label className={classNames('post-error', {errorClass})}>{postError}</label>} {postError && (
{serverErrorJsx} <label className={classNames('post-error', {errorClass})}>
{postError}
</label>
)}
{serverError && (
<MessageSubmitError
error={serverError}
submittedMessage={serverError.submittedMessage}
handleSubmit={handleSubmit}
/>
)}
<MsgTyping <MsgTyping
channelId={channelId} channelId={channelId}
postId={postId} postId={postId}

View File

@ -3,17 +3,15 @@
import React, {MouseEvent, DragEvent, ChangeEvent} from 'react'; import React, {MouseEvent, DragEvent, ChangeEvent} from 'react';
import {FileInfo} from '@mattermost/types/files';
import {General} from 'mattermost-redux/constants'; import {General} from 'mattermost-redux/constants';
import {clearFileInput} from 'utils/utils';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import FileUpload, {FileUpload as FileUploadClass} from 'components/file_upload/file_upload'; import FileUpload, {FileUpload as FileUploadClass} from 'components/file_upload/file_upload';
import {clearFileInput} from 'utils/utils';
import {FilesWillUploadHook} from 'types/store/plugins'; import {FilesWillUploadHook} from 'types/store/plugins';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import {FileInfo} from '@mattermost/types/files';
const generatedIdRegex = /[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}/; const generatedIdRegex = /[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}/;
@ -266,7 +264,7 @@ describe('components/FileUpload', () => {
); );
expect(baseProps.onUploadError).toHaveBeenCalledTimes(1); expect(baseProps.onUploadError).toHaveBeenCalledTimes(1);
expect(baseProps.onUploadError).toHaveBeenCalledWith(''); expect(baseProps.onUploadError).toHaveBeenCalledWith(null);
}); });
test('should error max upload files', () => { test('should error max upload files', () => {
@ -286,7 +284,7 @@ describe('components/FileUpload', () => {
expect(baseProps.onUploadStart).toBeCalledWith([], props.channelId); expect(baseProps.onUploadStart).toBeCalledWith([], props.channelId);
expect(baseProps.onUploadError).toHaveBeenCalledTimes(2); expect(baseProps.onUploadError).toHaveBeenCalledTimes(2);
expect(baseProps.onUploadError.mock.calls[0][0]).toEqual(''); expect(baseProps.onUploadError.mock.calls[0][0]).toEqual(null);
}); });
test('should error max upload files', () => { test('should error max upload files', () => {
@ -306,7 +304,7 @@ describe('components/FileUpload', () => {
expect(baseProps.onUploadStart).toBeCalledWith([], props.channelId); expect(baseProps.onUploadStart).toBeCalledWith([], props.channelId);
expect(baseProps.onUploadError).toHaveBeenCalledTimes(2); expect(baseProps.onUploadError).toHaveBeenCalledTimes(2);
expect(baseProps.onUploadError.mock.calls[0][0]).toEqual(''); expect(baseProps.onUploadError.mock.calls[0][0]).toEqual(null);
}); });
test('should error max too large files', () => { test('should error max too large files', () => {
@ -324,7 +322,7 @@ describe('components/FileUpload', () => {
expect(baseProps.onUploadStart).toBeCalledWith([], baseProps.channelId); expect(baseProps.onUploadStart).toBeCalledWith([], baseProps.channelId);
expect(baseProps.onUploadError).toHaveBeenCalledTimes(2); expect(baseProps.onUploadError).toHaveBeenCalledTimes(2);
expect(baseProps.onUploadError.mock.calls[0][0]).toEqual(''); expect(baseProps.onUploadError.mock.calls[0][0]).toEqual(null);
}); });
test('should functions when handleChange is called', () => { test('should functions when handleChange is called', () => {
@ -358,7 +356,7 @@ describe('components/FileUpload', () => {
instance.handleDrop(e); instance.handleDrop(e);
expect(baseProps.onUploadError).toBeCalled(); expect(baseProps.onUploadError).toBeCalled();
expect(baseProps.onUploadError).toHaveBeenCalledWith(''); expect(baseProps.onUploadError).toHaveBeenCalledWith(null);
expect(instance.uploadFiles).toBeCalled(); expect(instance.uploadFiles).toBeCalled();
expect(instance.uploadFiles).toHaveBeenCalledWith(e.dataTransfer.files); expect(instance.uploadFiles).toHaveBeenCalledWith(e.dataTransfer.files);
@ -386,7 +384,7 @@ describe('components/FileUpload', () => {
expect(baseProps.onUploadStart).toHaveBeenCalledTimes(0); expect(baseProps.onUploadStart).toHaveBeenCalledTimes(0);
expect(baseProps.onUploadError).toHaveBeenCalledTimes(1); expect(baseProps.onUploadError).toHaveBeenCalledTimes(1);
expect(baseProps.onUploadError).toHaveBeenCalledWith(''); expect(baseProps.onUploadError).toHaveBeenCalledWith(null);
}); });
test('FilesWillUploadHook - should reject one file and allow one file', () => { test('FilesWillUploadHook - should reject one file and allow one file', () => {
@ -409,6 +407,6 @@ describe('components/FileUpload', () => {
expect(baseProps.onUploadStart).toHaveBeenCalledWith([expect.stringMatching(generatedIdRegex)], props.channelId); expect(baseProps.onUploadStart).toHaveBeenCalledWith([expect.stringMatching(generatedIdRegex)], props.channelId);
expect(baseProps.onUploadError).toHaveBeenCalledTimes(1); expect(baseProps.onUploadError).toHaveBeenCalledTimes(1);
expect(baseProps.onUploadError).toHaveBeenCalledWith(''); expect(baseProps.onUploadError).toHaveBeenCalledWith(null);
}); });
}); });

View File

@ -120,7 +120,7 @@ export type Props = {
/** /**
* Function to be called when upload fails * Function to be called when upload fails
*/ */
onUploadError: (err: string | ServerError, clientId?: string, channelId?: string, currentRootId?: string) => void; onUploadError: (err: string | ServerError | null, clientId?: string, channelId?: string, currentRootId?: string) => void;
/** /**
* Function to be called when file upload starts * Function to be called when file upload starts
@ -222,13 +222,13 @@ export class FileUpload extends PureComponent<Props, State> {
pluginUploadFiles = (files: File[]) => { pluginUploadFiles = (files: File[]) => {
// clear any existing errors // clear any existing errors
this.props.onUploadError(''); this.props.onUploadError(null);
this.uploadFiles(files); this.uploadFiles(files);
}; };
checkPluginHooksAndUploadFiles = (files: FileList | File[]) => { checkPluginHooksAndUploadFiles = (files: FileList | File[]) => {
// clear any existing errors // clear any existing errors
this.props.onUploadError(''); this.props.onUploadError(null);
let sortedFiles = Array.from(files).sort((a, b) => a.name.localeCompare(b.name, this.props.locale, {numeric: true})); let sortedFiles = Array.from(files).sort((a, b) => a.name.localeCompare(b.name, this.props.locale, {numeric: true}));
@ -335,7 +335,7 @@ export class FileUpload extends PureComponent<Props, State> {
return; return;
} }
this.props.onUploadError(''); this.props.onUploadError(null);
const items = e.dataTransfer.items || []; const items = e.dataTransfer.items || [];
const droppedFiles = e.dataTransfer.files; const droppedFiles = e.dataTransfer.files;
@ -460,7 +460,7 @@ export class FileUpload extends PureComponent<Props, State> {
return; return;
} }
this.props.onUploadError(''); this.props.onUploadError(null);
const items = []; const items = [];
for (let i = 0; i < e.clipboardData.items.length; i++) { for (let i = 0; i < e.clipboardData.items.length; i++) {

View File

@ -1,68 +1,56 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {ReactFragment} from 'react'; import React, {MouseEventHandler} from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import {ServerError} from '@mattermost/types/errors'; import {ServerError} from '@mattermost/types/errors';
import {isErrorInvalidSlashCommand} from 'utils/post_utils'; import {isErrorInvalidSlashCommand} from 'utils/post_utils';
interface MessageSubmitErrorProps { interface Props {
error: ServerError; error: ServerError;
handleSubmit: (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void; handleSubmit: MouseEventHandler<HTMLAnchorElement>;
submittedMessage?: string; submittedMessage?: string;
} }
class MessageSubmitError extends React.PureComponent<MessageSubmitErrorProps> { function MessageSubmitError(props: Props) {
public renderSlashCommandError = (): string | ReactFragment => { if (isErrorInvalidSlashCommand(props.error)) {
if (!this.props.submittedMessage) { const slashCommand = props.submittedMessage?.split(' ')[0];
return this.props.error.message;
}
const command = this.props.submittedMessage.split(' ')[0];
return (
<React.Fragment>
<FormattedMessage
id='message_submit_error.invalidCommand'
defaultMessage="Command with a trigger of ''{command}'' not found. "
values={{
command,
}}
/>
<a
href='#'
onClick={this.props.handleSubmit}
>
<FormattedMessage
id='message_submit_error.sendAsMessageLink'
defaultMessage='Click here to send as a message.'
/>
</a>
</React.Fragment>
);
};
public render(): JSX.Element | null {
const error = this.props.error;
if (!error) {
return null;
}
let errorContent: string | ReactFragment = error.message;
if (isErrorInvalidSlashCommand(error)) {
errorContent = this.renderSlashCommandError();
}
return ( return (
<div className='has-error'> <div className='has-error'>
<label className='control-label'> <label className='control-label'>
{errorContent} <FormattedMessage
id='message_submit_error.invalidCommand'
defaultMessage="Command with a trigger of ''{slashCommand}'' not found. "
values={{
slashCommand,
}}
/>
<a
href='#'
onClick={props.handleSubmit}
>
<FormattedMessage
id='message_submit_error.sendAsMessageLink'
defaultMessage='Click here to send as a message.'
/>
</a>
</label> </label>
</div> </div>
); );
} }
if (props.error?.message?.trim()?.length === 0) {
return null;
}
return (
<div className='has-error'>
<label className='control-label'>{props.error.message.trim()}</label>
</div>
);
} }
export default MessageSubmitError; export default MessageSubmitError;