Unify handlePaste and handlekeydown (#24511)
* Unify paste and handlekeydown * Fix snapshots * Address feedback * Properly fix MM-54178 and avoid opening the reaction from the edit post textbox
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c1eb40ae05
Коммит
756765b9b4
@@ -10,7 +10,9 @@ exports[`components/AdvancedCreateComment should match snapshot when cannot post
|
||||
badConnection={false}
|
||||
canPost={false}
|
||||
canUploadFiles={true}
|
||||
caretPosition={12}
|
||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
||||
ctrlSend={false}
|
||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
||||
draft={
|
||||
Object {
|
||||
@@ -88,7 +90,6 @@ exports[`components/AdvancedCreateComment should match snapshot when cannot post
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -97,9 +98,13 @@ exports[`components/AdvancedCreateComment should match snapshot when cannot post
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="RHS_COMMENT"
|
||||
maxPostSize={4000}
|
||||
message="Test message"
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
removePreview={[Function]}
|
||||
@@ -130,7 +135,9 @@ exports[`components/AdvancedCreateComment should match snapshot, comment with me
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={true}
|
||||
caretPosition={12}
|
||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
||||
ctrlSend={true}
|
||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
||||
draft={
|
||||
Object {
|
||||
@@ -159,7 +166,6 @@ exports[`components/AdvancedCreateComment should match snapshot, comment with me
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -168,9 +174,13 @@ exports[`components/AdvancedCreateComment should match snapshot, comment with me
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="RHS_COMMENT"
|
||||
maxPostSize={4000}
|
||||
message="Test message"
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
removePreview={[Function]}
|
||||
@@ -202,7 +212,9 @@ exports[`components/AdvancedCreateComment should match snapshot, emoji picker di
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={true}
|
||||
caretPosition={12}
|
||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
||||
ctrlSend={false}
|
||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
||||
draft={
|
||||
Object {
|
||||
@@ -280,7 +292,6 @@ exports[`components/AdvancedCreateComment should match snapshot, emoji picker di
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -289,9 +300,13 @@ exports[`components/AdvancedCreateComment should match snapshot, emoji picker di
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="RHS_COMMENT"
|
||||
maxPostSize={4000}
|
||||
message="Test message"
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
removePreview={[Function]}
|
||||
@@ -322,7 +337,9 @@ exports[`components/AdvancedCreateComment should match snapshot, empty comment 1
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={true}
|
||||
caretPosition={0}
|
||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
||||
ctrlSend={true}
|
||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
||||
draft={
|
||||
Object {
|
||||
@@ -351,7 +368,6 @@ exports[`components/AdvancedCreateComment should match snapshot, empty comment 1
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -360,9 +376,13 @@ exports[`components/AdvancedCreateComment should match snapshot, empty comment 1
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="RHS_COMMENT"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
removePreview={[Function]}
|
||||
@@ -394,7 +414,9 @@ exports[`components/AdvancedCreateComment should match snapshot, non-empty messa
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={true}
|
||||
caretPosition={12}
|
||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
||||
ctrlSend={false}
|
||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
||||
draft={
|
||||
Object {
|
||||
@@ -472,7 +494,6 @@ exports[`components/AdvancedCreateComment should match snapshot, non-empty messa
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -481,9 +502,13 @@ exports[`components/AdvancedCreateComment should match snapshot, non-empty messa
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="RHS_COMMENT"
|
||||
maxPostSize={4000}
|
||||
message="Test message"
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
removePreview={[Function]}
|
||||
|
||||
@@ -11,12 +11,9 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import type {Props} from 'components/advanced_create_comment/advanced_create_comment';
|
||||
import AdvancedCreateComment from 'components/advanced_create_comment/advanced_create_comment';
|
||||
import AdvanceTextEditor from 'components/advanced_text_editor/advanced_text_editor';
|
||||
|
||||
import {testComponentForLineBreak} from 'tests/helpers/line_break_helpers';
|
||||
import {testComponentForMarkdownHotkeys} from 'tests/helpers/markdown_hotkey_helpers.js';
|
||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||
import {execCommandInsertText} from 'utils/exec_commands';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import type {PostDraft} from 'types/store/draft';
|
||||
@@ -65,8 +62,8 @@ describe('components/AdvancedCreateComment', () => {
|
||||
updateCommentDraftWithRootId: jest.fn(),
|
||||
onSubmit: jest.fn(),
|
||||
onResetHistoryIndex: jest.fn(),
|
||||
onMoveHistoryIndexBack: jest.fn(),
|
||||
onMoveHistoryIndexForward: jest.fn(),
|
||||
moveHistoryIndexBack: jest.fn(),
|
||||
moveHistoryIndexForward: jest.fn(),
|
||||
onEditLatestPost: jest.fn(),
|
||||
resetCreatePostRequest: jest.fn(),
|
||||
setShowPreview: jest.fn(),
|
||||
@@ -1208,99 +1205,6 @@ describe('components/AdvancedCreateComment', () => {
|
||||
expect(instance.focusTextbox).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should call functions on handleKeyDown', () => {
|
||||
const onMoveHistoryIndexBack = jest.fn();
|
||||
const onMoveHistoryIndexForward = jest.fn();
|
||||
const onEditLatestPost = jest.fn().
|
||||
mockImplementationOnce(() => ({data: true})).
|
||||
mockImplementationOnce(() => ({data: false}));
|
||||
const wrapper = shallow<AdvancedCreateComment>(
|
||||
<AdvancedCreateComment
|
||||
{...baseProps}
|
||||
ctrlSend={true}
|
||||
onMoveHistoryIndexBack={onMoveHistoryIndexBack}
|
||||
onMoveHistoryIndexForward={onMoveHistoryIndexForward}
|
||||
onEditLatestPost={onEditLatestPost}
|
||||
/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
instance.commentMsgKeyPress = jest.fn();
|
||||
instance.focusTextbox = jest.fn();
|
||||
const blur = jest.fn();
|
||||
|
||||
const mockImpl = () => {
|
||||
return {
|
||||
setSelectionRange: jest.fn(),
|
||||
getBoundingClientRect: jest.fn(mockTop),
|
||||
blur: jest.fn(),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
const mockTop = () => {
|
||||
return document.createElement('div');
|
||||
};
|
||||
|
||||
(instance as any).textboxRef.current = {blur, focus, getInputBox: jest.fn(mockImpl)};
|
||||
|
||||
const mockTarget = {
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
value: 'brown\nfox jumps over lazy dog',
|
||||
};
|
||||
|
||||
const commentMsgKey: any = {
|
||||
preventDefault: jest.fn(),
|
||||
ctrlKey: true,
|
||||
key: Constants.KeyCodes.ENTER[0],
|
||||
keyCode: Constants.KeyCodes.ENTER[1],
|
||||
target: mockTarget,
|
||||
};
|
||||
instance.handleKeyDown(commentMsgKey);
|
||||
expect(instance.commentMsgKeyPress).toHaveBeenCalledTimes(1);
|
||||
|
||||
const upKey: any = {
|
||||
preventDefault: jest.fn(),
|
||||
ctrlKey: true,
|
||||
key: Constants.KeyCodes.UP[0],
|
||||
keyCode: Constants.KeyCodes.UP[1],
|
||||
target: mockTarget,
|
||||
};
|
||||
instance.handleKeyDown(upKey);
|
||||
expect(upKey.preventDefault).toHaveBeenCalledTimes(1);
|
||||
expect(onMoveHistoryIndexBack).toHaveBeenCalledTimes(1);
|
||||
|
||||
const downKey = {
|
||||
preventDefault: jest.fn(),
|
||||
ctrlKey: true,
|
||||
key: Constants.KeyCodes.DOWN[0],
|
||||
keyCode: Constants.KeyCodes.DOWN[1],
|
||||
target: mockTarget,
|
||||
};
|
||||
instance.handleKeyDown(downKey as any);
|
||||
expect(downKey.preventDefault).toHaveBeenCalledTimes(1);
|
||||
expect(onMoveHistoryIndexForward).toHaveBeenCalledTimes(1);
|
||||
|
||||
wrapper.setState({draft: emptyDraft});
|
||||
const upKeyForEdit: any = {
|
||||
preventDefault: jest.fn(),
|
||||
ctrlKey: false,
|
||||
key: Constants.KeyCodes.UP[0],
|
||||
keyCode: Constants.KeyCodes.UP[1],
|
||||
target: mockTarget,
|
||||
};
|
||||
instance.handleKeyDown(upKeyForEdit);
|
||||
expect(upKeyForEdit.preventDefault).toHaveBeenCalledTimes(1);
|
||||
expect(onEditLatestPost).toHaveBeenCalledTimes(1);
|
||||
expect(blur).toHaveBeenCalledTimes(1);
|
||||
|
||||
instance.handleKeyDown(upKeyForEdit);
|
||||
expect(upKeyForEdit.preventDefault).toHaveBeenCalledTimes(2);
|
||||
expect(onEditLatestPost).toHaveBeenCalledTimes(2);
|
||||
expect(instance.focusTextbox).toHaveBeenCalledTimes(1);
|
||||
expect(instance.focusTextbox).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('should the RHS thread scroll to bottom one time after mount when props.draft.message is not empty', () => {
|
||||
const draft: PostDraft = emptyDraft;
|
||||
const scrollToBottom = jest.fn();
|
||||
@@ -1348,181 +1252,6 @@ describe('components/AdvancedCreateComment', () => {
|
||||
expect(scrollToBottom).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
test('should be able to format a pasted markdown table', () => {
|
||||
const draft: PostDraft = emptyDraft;
|
||||
const wrapper = shallow<AdvancedCreateComment>(
|
||||
<AdvancedCreateComment
|
||||
{...baseProps}
|
||||
draft={draft}
|
||||
/>,
|
||||
);
|
||||
|
||||
const mockTop = () => {
|
||||
return document.createElement('div');
|
||||
};
|
||||
|
||||
const mockImpl = () => {
|
||||
return {
|
||||
setSelectionRange: jest.fn(),
|
||||
getBoundingClientRect: jest.fn(mockTop),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
(wrapper.instance() as any).textboxRef.current = {getInputBox: jest.fn(mockImpl), focus: jest.fn(), blur: jest.fn()};
|
||||
|
||||
const event: any = {
|
||||
target: {
|
||||
id: 'reply_textbox',
|
||||
},
|
||||
preventDefault: jest.fn(),
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/html'],
|
||||
getData: () => {
|
||||
return '<table><tr><th>test</th><th>test</th></tr><tr><td>test</td><td>test</td></tr></table>';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |';
|
||||
|
||||
wrapper.instance().pasteHandler(event);
|
||||
expect(execCommandInsertText).toHaveBeenCalledWith(markdownTable);
|
||||
});
|
||||
|
||||
test('should be able to format a pasted markdown table without headers', () => {
|
||||
const draft: PostDraft = emptyDraft;
|
||||
const wrapper = shallow<AdvancedCreateComment>(
|
||||
<AdvancedCreateComment
|
||||
{...baseProps}
|
||||
draft={draft}
|
||||
/>,
|
||||
);
|
||||
|
||||
const mockTop = () => {
|
||||
return document.createElement('div');
|
||||
};
|
||||
|
||||
const mockImpl = () => {
|
||||
return {
|
||||
setSelectionRange: jest.fn(),
|
||||
getBoundingClientRect: jest.fn(mockTop),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
(wrapper.instance() as any).textboxRef.current = {getInputBox: jest.fn(mockImpl), focus: jest.fn(), blur: jest.fn()};
|
||||
|
||||
const event: any = {
|
||||
target: {
|
||||
id: 'reply_textbox',
|
||||
},
|
||||
preventDefault: jest.fn(),
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/html'],
|
||||
getData: () => {
|
||||
return '<table><tr><td>test</td><td>test</td></tr><tr><td>test</td><td>test</td></tr></table>';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |\n';
|
||||
|
||||
wrapper.instance().pasteHandler(event);
|
||||
expect(execCommandInsertText).toHaveBeenCalledWith(markdownTable);
|
||||
});
|
||||
|
||||
test('should be able to format a pasted hyperlink', () => {
|
||||
const draft: PostDraft = emptyDraft;
|
||||
const wrapper = shallow<AdvancedCreateComment>(
|
||||
<AdvancedCreateComment
|
||||
{...baseProps}
|
||||
draft={draft}
|
||||
/>,
|
||||
);
|
||||
|
||||
const mockTop = () => {
|
||||
return document.createElement('div');
|
||||
};
|
||||
|
||||
const mockImpl = () => {
|
||||
return {
|
||||
setSelectionRange: jest.fn(),
|
||||
getBoundingClientRect: jest.fn(mockTop),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
(wrapper.instance() as any).textboxRef.current = {getInputBox: jest.fn(mockImpl), focus: jest.fn(), blur: jest.fn()};
|
||||
|
||||
const event: any = {
|
||||
target: {
|
||||
id: 'reply_textbox',
|
||||
},
|
||||
preventDefault: jest.fn(),
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/html'],
|
||||
getData: () => {
|
||||
return '<a href="https://test.domain">link text</a>';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const markdownLink = '[link text](https://test.domain)';
|
||||
|
||||
wrapper.instance().pasteHandler(event);
|
||||
expect(execCommandInsertText).toHaveBeenCalledWith(markdownLink);
|
||||
});
|
||||
|
||||
test('should be able to format a github codeblock (pasted as a table)', () => {
|
||||
const draft: PostDraft = emptyDraft;
|
||||
const wrapper = shallow<AdvancedCreateComment>(
|
||||
<AdvancedCreateComment
|
||||
{...baseProps}
|
||||
draft={draft}
|
||||
/>,
|
||||
);
|
||||
|
||||
const mockTop = () => {
|
||||
return document.createElement('div');
|
||||
};
|
||||
|
||||
const mockImpl = () => {
|
||||
return {
|
||||
setSelectionRange: jest.fn(),
|
||||
getBoundingClientRect: jest.fn(mockTop),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
(wrapper.instance() as any).textboxRef.current = {getInputBox: jest.fn(mockImpl), focus: jest.fn(), blur: jest.fn()};
|
||||
|
||||
const event: any = {
|
||||
target: {
|
||||
id: 'reply_textbox',
|
||||
},
|
||||
preventDefault: jest.fn(),
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/plain', 'text/html'],
|
||||
getData: (type: any) => {
|
||||
if (type === 'text/plain') {
|
||||
return '// a javascript codeblock example\nif (1 > 0) {\n return \'condition is true\';\n}';
|
||||
}
|
||||
return '<table class="highlight tab-size js-file-line-container" data-tab-size="8"><tbody><tr><td id="LC1" class="blob-code blob-code-inner js-file-line"><span class="pl-c"><span class="pl-c">//</span> a javascript codeblock example</span></td></tr><tr><td id="L2" class="blob-num js-line-number" data-line-number="2"> </td><td id="LC2" class="blob-code blob-code-inner js-file-line"><span class="pl-k">if</span> (<span class="pl-c1">1</span> <span class="pl-k">></span> <span class="pl-c1">0</span>) {</td></tr><tr><td id="L3" class="blob-num js-line-number" data-line-number="3"> </td><td id="LC3" class="blob-code blob-code-inner js-file-line"><span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span class="pl-pds">\'</span>condition is true<span class="pl-pds">\'</span></span>);</td></tr><tr><td id="L4" class="blob-num js-line-number" data-line-number="4"> </td><td id="LC4" class="blob-code blob-code-inner js-file-line">}</td></tr></tbody></table>';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const codeBlockMarkdown = "```\n// a javascript codeblock example\nif (1 > 0) {\n return 'condition is true';\n}\n```";
|
||||
|
||||
wrapper.instance().pasteHandler(event);
|
||||
expect(execCommandInsertText).toHaveBeenCalledWith(codeBlockMarkdown);
|
||||
});
|
||||
|
||||
test('should show preview and edit mode, and return focus on preview disable', () => {
|
||||
const wrapper = shallow<AdvancedCreateComment>(
|
||||
<AdvancedCreateComment {...baseProps}/>,
|
||||
@@ -1552,73 +1281,4 @@ describe('components/AdvancedCreateComment', () => {
|
||||
ctrlSend={true}
|
||||
/>
|
||||
), (instance: any) => instance.state().draft.message, false);
|
||||
|
||||
testComponentForMarkdownHotkeys(
|
||||
(value: any) => (
|
||||
<AdvancedCreateComment
|
||||
{...baseProps}
|
||||
draft={{
|
||||
...baseProps.draft,
|
||||
message: value,
|
||||
}}
|
||||
ctrlSend={true}
|
||||
/>
|
||||
),
|
||||
(wrapper: any, setSelectionRangeFn: any) => {
|
||||
const mockTop = () => {
|
||||
return document.createElement('div');
|
||||
};
|
||||
wrapper.instance().textboxRef = {
|
||||
current: {
|
||||
getInputBox: jest.fn(() => {
|
||||
return {
|
||||
focus: jest.fn(),
|
||||
getBoundingClientRect: jest.fn(mockTop),
|
||||
setSelectionRange: setSelectionRangeFn,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
(instance: any) => instance.find(AdvanceTextEditor),
|
||||
(instance: any) => instance.state().draft.message,
|
||||
false,
|
||||
'reply_textbox',
|
||||
);
|
||||
|
||||
it('should blur when ESCAPE is pressed', () => {
|
||||
const wrapper = shallow<AdvancedCreateComment>(
|
||||
<AdvancedCreateComment
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
const blur = jest.fn();
|
||||
|
||||
const mockImpl = () => {
|
||||
return {
|
||||
blur: jest.fn(),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
(instance as any).textboxRef.current = {blur, getInputBox: jest.fn(mockImpl)};
|
||||
|
||||
const mockTarget = {
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
value: 'brown\nfox jumps over lazy dog',
|
||||
};
|
||||
|
||||
const commentEscapeKey = {
|
||||
preventDefault: jest.fn(),
|
||||
ctrlKey: true,
|
||||
key: Constants.KeyCodes.ESCAPE[0],
|
||||
keyCode: Constants.KeyCodes.ESCAPE[1],
|
||||
target: mockTarget,
|
||||
};
|
||||
|
||||
instance.handleKeyDown(commentEscapeKey as any);
|
||||
expect(blur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import {isNil} from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import type {ChannelMemberCountsByGroup} from '@mattermost/types/channels';
|
||||
@@ -14,6 +13,7 @@ import {GroupSource} from '@mattermost/types/groups';
|
||||
import type {Group} from '@mattermost/types/groups';
|
||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
@@ -28,22 +28,11 @@ import PostDeletedModal from 'components/post_deleted_modal';
|
||||
import type {TextboxClass, TextboxElement} from 'components/textbox';
|
||||
|
||||
import Constants, {AdvancedTextEditor as AdvancedTextEditorConst, Locations, ModalIdentifiers, Preferences} from 'utils/constants';
|
||||
import {execCommandInsertText} from 'utils/exec_commands';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import {
|
||||
applyMarkdown,
|
||||
} from 'utils/markdown/apply_markdown';
|
||||
import type {
|
||||
ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
||||
import {
|
||||
getHtmlTable,
|
||||
hasHtmlLink,
|
||||
formatMarkdownMessage,
|
||||
isGitHubCodeBlock,
|
||||
formatGithubCodePaste,
|
||||
isTextUrl,
|
||||
formatMarkdownLinkMessage,
|
||||
} from 'utils/paste';
|
||||
import {
|
||||
specialMentionsInText,
|
||||
postMessageOnKeyPress,
|
||||
@@ -52,7 +41,6 @@ import {
|
||||
splitMessageBasedOnCaretPosition,
|
||||
groupsMentionedInText,
|
||||
mentionsMinusSpecialMentionsInText,
|
||||
isWithinCodeBlock,
|
||||
} from 'utils/post_utils';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import * as Utils from 'utils/utils';
|
||||
@@ -61,8 +49,6 @@ import type {ModalData} from 'types/actions';
|
||||
import type {PostDraft} from 'types/store/draft';
|
||||
import type {PluginComponent} from 'types/store/plugins';
|
||||
|
||||
const KeyCodes = Constants.KeyCodes;
|
||||
|
||||
export type Props = {
|
||||
currentTeamId: string;
|
||||
|
||||
@@ -130,10 +116,10 @@ export type Props = {
|
||||
onResetHistoryIndex: () => void;
|
||||
|
||||
// Called when navigating back through comment message history
|
||||
onMoveHistoryIndexBack: () => void;
|
||||
moveHistoryIndexBack: (index: string) => Promise<void>;
|
||||
|
||||
// Called when navigating forward through comment message history
|
||||
onMoveHistoryIndexForward: () => void;
|
||||
moveHistoryIndexForward: (index: string) => Promise<void>;
|
||||
|
||||
// Called to initiate editing the user's latest post
|
||||
onEditLatestPost: () => ActionResult;
|
||||
@@ -206,11 +192,11 @@ type State = {
|
||||
uploadsProgressPercent: {[clientID: string]: FilePreviewInfo};
|
||||
renderScrollbar: boolean;
|
||||
scrollbarWidth: number;
|
||||
draft?: PostDraft;
|
||||
draft: PostDraft;
|
||||
rootId?: string;
|
||||
messageInHistory?: string;
|
||||
createPostErrorId?: string;
|
||||
caretPosition?: number;
|
||||
caretPosition: number;
|
||||
postError?: React.ReactNode;
|
||||
errorClass: string | null;
|
||||
serverError: (ServerError & {submittedMessage?: string}) | null;
|
||||
@@ -248,7 +234,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
createPostErrorId: props.createPostErrorId,
|
||||
rootId: props.rootId,
|
||||
messageInHistory: props.messageInHistory,
|
||||
draft: state.draft || {...props.draft, caretPosition: props.draft.message.length, uploadsInProgress: []},
|
||||
};
|
||||
|
||||
const rootChanged = props.rootId !== state.rootId || props.draft.rootId !== state.draft?.rootId;
|
||||
@@ -278,7 +263,8 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
serverError: null,
|
||||
showFormat: false,
|
||||
isFormattingBarHidden: props.isFormattingBarHidden,
|
||||
caretPosition: props.draft.caretPosition,
|
||||
caretPosition: props.draft.message.length,
|
||||
draft: {...props.draft, uploadsInProgress: []},
|
||||
};
|
||||
|
||||
this.textboxRef = React.createRef();
|
||||
@@ -295,7 +281,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
this.focusTextbox();
|
||||
}
|
||||
|
||||
document.addEventListener('paste', this.pasteHandler);
|
||||
document.addEventListener('keydown', this.focusTextboxIfNecessary);
|
||||
window.addEventListener('beforeunload', this.saveDraftWithShow);
|
||||
this.getChannelMemberCountsByGroup();
|
||||
@@ -310,7 +295,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.resetCreatePostRequest?.();
|
||||
document.removeEventListener('paste', this.pasteHandler);
|
||||
document.removeEventListener('keydown', this.focusTextboxIfNecessary);
|
||||
window.removeEventListener('beforeunload', this.saveDraftWithShow);
|
||||
this.saveDraftOnUnmount();
|
||||
@@ -351,6 +335,26 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
fillMessageFromHistory() {
|
||||
const lastMessage = this.props.messageInHistory;
|
||||
this.setState((prev) => ({
|
||||
draft: {
|
||||
...prev.draft,
|
||||
message: lastMessage || '',
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
loadPrevMessage = (e: React.KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
this.props.moveHistoryIndexBack(Posts.MESSAGE_TYPES.COMMENT).then(() => this.fillMessageFromHistory());
|
||||
};
|
||||
|
||||
loadNextMessage = (e: React.KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
this.props.moveHistoryIndexForward(Posts.MESSAGE_TYPES.COMMENT).then(() => this.fillMessageFromHistory());
|
||||
};
|
||||
|
||||
getChannelMemberCountsByGroup = () => {
|
||||
const {useLDAPGroupMentions, useCustomGroupMentions, channelId, isTimezoneEnabled, searchAssociatedGroupsForReference, getChannelMemberCountsByGroup, draft, currentTeamId} = this.props;
|
||||
|
||||
@@ -438,43 +442,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
pasteHandler = (event: ClipboardEvent) => {
|
||||
const {clipboardData, target} = event;
|
||||
|
||||
if (!clipboardData || !clipboardData.items || !target || (target as TextboxElement)?.id !== 'reply_textbox') {
|
||||
return;
|
||||
}
|
||||
|
||||
const {selectionStart, selectionEnd} = target as TextboxElement;
|
||||
|
||||
const hasSelection = !isNil(selectionStart) && !isNil(selectionEnd) && selectionStart < selectionEnd;
|
||||
const hasTextUrl = isTextUrl(clipboardData);
|
||||
const hasHTMLLinks = !this.isNonFormattedPaste && hasHtmlLink(clipboardData);
|
||||
const htmlTable = getHtmlTable(clipboardData);
|
||||
const shouldApplyLinkMarkdown = hasSelection && hasTextUrl;
|
||||
const shouldApplyGithubCodeBlock = htmlTable && isGitHubCodeBlock(htmlTable.className);
|
||||
|
||||
if (!htmlTable && !hasHTMLLinks && !shouldApplyLinkMarkdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const message = this.state.draft?.message ?? '';
|
||||
|
||||
// execCommand's insertText' triggers a 'change' event, hence we need not set respective state explicitly.
|
||||
if (shouldApplyLinkMarkdown) {
|
||||
const formattedLink = formatMarkdownLinkMessage({selectionStart, selectionEnd, message, clipboardData});
|
||||
execCommandInsertText(formattedLink);
|
||||
} else if (shouldApplyGithubCodeBlock) {
|
||||
const {formattedCodeBlock} = formatGithubCodePaste({selectionStart, selectionEnd, message, clipboardData});
|
||||
execCommandInsertText(formattedCodeBlock);
|
||||
} else {
|
||||
const {formattedMarkdown} = formatMarkdownMessage(clipboardData, message, this.state.caretPosition);
|
||||
execCommandInsertText(formattedMarkdown);
|
||||
}
|
||||
};
|
||||
|
||||
handleNotifyAllConfirmation = () => {
|
||||
this.doSubmit();
|
||||
};
|
||||
@@ -841,188 +808,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
handleKeyDown = (e: React.KeyboardEvent<TextboxElement>) => {
|
||||
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||
const lastMessageReactionKeyCombo = ctrlOrMetaKeyPressed && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||
|
||||
const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const shiftAltCombo = !Keyboard.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||
|
||||
// fix for FF not capturing the paste without formatting event when using ctrl|cmd + shift + v
|
||||
if (e.key === KeyCodes.V[0] && ctrlOrMetaKeyPressed) {
|
||||
if (e.shiftKey) {
|
||||
this.isNonFormattedPaste = true;
|
||||
this.timeoutId = window.setTimeout(() => {
|
||||
this.isNonFormattedPaste = false;
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
|
||||
// listen for line break key combo and insert new line character
|
||||
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||
this.setState({
|
||||
draft: {
|
||||
...this.state.draft!,
|
||||
message: Utils.insertLineBreakFromKeyEvent(e as React.KeyboardEvent<HTMLTextAreaElement>),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(this.props.ctrlSend || this.props.codeBlockOnCtrlEnter) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
(e.ctrlKey || e.metaKey)
|
||||
) {
|
||||
this.setShowPreview(false);
|
||||
this.commentMsgKeyPress(e);
|
||||
return;
|
||||
}
|
||||
|
||||
const draft = this.state.draft!;
|
||||
const {message} = draft;
|
||||
const {caretPosition} = this.state;
|
||||
const caretIsWithinCodeBlock = caretPosition && isWithinCodeBlock(message, caretPosition);
|
||||
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||
this.textboxRef.current?.blur();
|
||||
}
|
||||
|
||||
if (
|
||||
!e.ctrlKey &&
|
||||
!e.metaKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.UP) &&
|
||||
message === ''
|
||||
) {
|
||||
e.preventDefault();
|
||||
if (this.textboxRef.current) {
|
||||
this.textboxRef.current.blur();
|
||||
}
|
||||
|
||||
const {data: canEditNow} = this.props.onEditLatestPost();
|
||||
if (!canEditNow) {
|
||||
this.focusTextbox(true);
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
value,
|
||||
} = e.target as TextboxElement;
|
||||
|
||||
if (ctrlKeyCombo && !caretIsWithinCodeBlock) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.UP)) {
|
||||
e.preventDefault();
|
||||
this.props.onMoveHistoryIndexBack();
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
e.preventDefault();
|
||||
this.props.onMoveHistoryIndexForward();
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.B)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'bold',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.I)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'italic',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isTextSelectedInPostOrReply(e) && Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'link',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
}
|
||||
} else if (ctrlAltCombo && !caretIsWithinCodeBlock) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'link',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.C)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'code',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleEmojiPicker();
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.T)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleAdvanceTextEditor();
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.P) && draft.message.length) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setShowPreview(!this.props.shouldShowPreview);
|
||||
}
|
||||
} else if (shiftAltCombo && !caretIsWithinCodeBlock) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.X)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'strike',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ol',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ul',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.NINE)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'quote',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (lastMessageReactionKeyCombo) {
|
||||
this.reactToLastMessage(e);
|
||||
}
|
||||
};
|
||||
|
||||
applyMarkdown = (options: ApplyMarkdownOptions) => {
|
||||
if (this.props.shouldShowPreview) {
|
||||
return;
|
||||
@@ -1221,6 +1006,25 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
this.lastBlurAt = Date.now();
|
||||
};
|
||||
|
||||
handleEditLatestPost = () => {
|
||||
const {data: canEditNow} = this.props.onEditLatestPost();
|
||||
if (!canEditNow) {
|
||||
this.focusTextbox(true);
|
||||
}
|
||||
};
|
||||
|
||||
onMessageChange = (message: string, callback?: (() => void) | undefined) => {
|
||||
const draft = this.state.draft;
|
||||
const modifiedDraft = {
|
||||
...draft,
|
||||
message,
|
||||
};
|
||||
this.handleDraftChange(modifiedDraft);
|
||||
this.setState({
|
||||
draft: modifiedDraft,
|
||||
}, callback);
|
||||
};
|
||||
|
||||
render() {
|
||||
const draft = this.state.draft!;
|
||||
|
||||
@@ -1295,7 +1099,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
handlePostError={this.handlePostError}
|
||||
emitTypingEvent={this.emitTypingEvent}
|
||||
handleMouseUpKeyUp={this.handleMouseUpKeyUp}
|
||||
handleKeyDown={this.handleKeyDown}
|
||||
postMsgKeyPress={this.commentMsgKeyPress}
|
||||
handleChange={this.handleChange}
|
||||
toggleEmojiPicker={this.toggleEmojiPicker}
|
||||
@@ -1312,6 +1115,13 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
||||
fileUploadRef={this.fileUploadRef}
|
||||
isThreadView={this.props.isThreadView}
|
||||
additionalControls={pluginItems.filter(Boolean)}
|
||||
codeBlockOnCtrlEnter={this.props.codeBlockOnCtrlEnter}
|
||||
ctrlSend={this.props.ctrlSend}
|
||||
loadNextMessage={this.loadNextMessage}
|
||||
loadPrevMessage={this.loadPrevMessage}
|
||||
onEditLatestPost={this.handleEditLatestPost}
|
||||
onMessageChange={this.onMessageChange}
|
||||
caretPosition={this.state.caretPosition}
|
||||
placeholder={this.props.placeholder}
|
||||
/>
|
||||
</form>
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {ActionCreatorsMapObject, Dispatch} from 'redux';
|
||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import {getChannelTimezones, getChannelMemberCountsByGroup} from 'mattermost-redux/actions/channels';
|
||||
import {resetCreatePostRequest, resetHistoryIndex} from 'mattermost-redux/actions/posts';
|
||||
import {moveHistoryIndexBack, moveHistoryIndexForward, resetCreatePostRequest, resetHistoryIndex} from 'mattermost-redux/actions/posts';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {Permissions, Preferences, Posts} from 'mattermost-redux/constants';
|
||||
import {getAllChannelStats, getChannelMemberCountsByGroup as selectChannelMemberCountsByGroup} from 'mattermost-redux/selectors/entities/channels';
|
||||
@@ -25,7 +25,6 @@ import {emitShortcutReactToLastPostFrom} from 'actions/post_actions';
|
||||
import {
|
||||
clearCommentDraftUploads,
|
||||
updateCommentDraft,
|
||||
makeOnMoveHistoryIndex,
|
||||
makeOnSubmit,
|
||||
makeOnEditLatestPost,
|
||||
} from 'actions/views/create_comment';
|
||||
@@ -132,8 +131,8 @@ type Actions = {
|
||||
updateCommentDraftWithRootId: (rootID: string, draft: PostDraft, save?: boolean) => void;
|
||||
onSubmit: (draft: PostDraft, options: {ignoreSlash: boolean}) => void;
|
||||
onResetHistoryIndex: () => void;
|
||||
onMoveHistoryIndexBack: () => void;
|
||||
onMoveHistoryIndexForward: () => void;
|
||||
moveHistoryIndexBack: (index: string) => Promise<void>;
|
||||
moveHistoryIndexForward: (index: string) => Promise<void>;
|
||||
onEditLatestPost: () => ActionResult;
|
||||
resetCreatePostRequest: () => void;
|
||||
getChannelTimezones: (channelId: string) => Promise<ActionResult>;
|
||||
@@ -152,14 +151,6 @@ function makeMapDispatchToProps() {
|
||||
draft: PostDraft,
|
||||
options: {ignoreSlash: boolean},
|
||||
) => (dispatch: DispatchFunc, getState: () => GlobalState) => Promise<ActionResult | ActionResult[]> | ActionResult;
|
||||
let onMoveHistoryIndexBack: () => (
|
||||
dispatch: DispatchFunc,
|
||||
getState: () => GlobalState,
|
||||
) => Promise<ActionResult | ActionResult[]> | ActionResult;
|
||||
let onMoveHistoryIndexForward: () => (
|
||||
dispatch: DispatchFunc,
|
||||
getState: () => GlobalState,
|
||||
) => Promise<ActionResult | ActionResult[]> | ActionResult;
|
||||
let onEditLatestPost: () => ActionFunc;
|
||||
|
||||
function onResetHistoryIndex() {
|
||||
@@ -173,8 +164,6 @@ function makeMapDispatchToProps() {
|
||||
return (dispatch: Dispatch, ownProps: OwnProps) => {
|
||||
if (rootId !== ownProps.rootId) {
|
||||
onUpdateCommentDraft = makeOnUpdateCommentDraft(ownProps.rootId, ownProps.channelId);
|
||||
onMoveHistoryIndexBack = makeOnMoveHistoryIndex(ownProps.rootId, -1);
|
||||
onMoveHistoryIndexForward = makeOnMoveHistoryIndex(ownProps.rootId, 1);
|
||||
}
|
||||
|
||||
if (channelId !== ownProps.channelId) {
|
||||
@@ -200,8 +189,8 @@ function makeMapDispatchToProps() {
|
||||
updateCommentDraftWithRootId,
|
||||
onSubmit,
|
||||
onResetHistoryIndex,
|
||||
onMoveHistoryIndexBack,
|
||||
onMoveHistoryIndexForward,
|
||||
moveHistoryIndexBack,
|
||||
moveHistoryIndexForward,
|
||||
onEditLatestPost,
|
||||
resetCreatePostRequest,
|
||||
getChannelTimezones,
|
||||
|
||||
@@ -13,7 +13,9 @@ exports[`components/advanced_create_post Show tutorial 1`] = `
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -62,7 +64,6 @@ exports[`components/advanced_create_post Show tutorial 1`] = `
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -71,13 +72,18 @@ exports[`components/advanced_create_post Show tutorial 1`] = `
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
@@ -109,7 +115,9 @@ exports[`components/advanced_create_post should match snapshot for center textbo
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -158,7 +166,6 @@ exports[`components/advanced_create_post should match snapshot for center textbo
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -167,13 +174,18 @@ exports[`components/advanced_create_post should match snapshot for center textbo
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
@@ -205,7 +217,9 @@ exports[`components/advanced_create_post should match snapshot when cannot post
|
||||
badConnection={false}
|
||||
canPost={false}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -254,7 +268,6 @@ exports[`components/advanced_create_post should match snapshot when cannot post
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -263,13 +276,18 @@ exports[`components/advanced_create_post should match snapshot when cannot post
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
@@ -301,7 +319,9 @@ exports[`components/advanced_create_post should match snapshot when file upload
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -350,7 +370,6 @@ exports[`components/advanced_create_post should match snapshot when file upload
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -359,13 +378,18 @@ exports[`components/advanced_create_post should match snapshot when file upload
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
@@ -397,7 +421,9 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -446,7 +472,6 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -455,13 +480,18 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
@@ -493,7 +523,9 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -542,7 +574,6 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -551,13 +582,18 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
@@ -589,7 +625,9 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
||||
badConnection={false}
|
||||
canPost={false}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -638,7 +676,6 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -647,13 +684,18 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
@@ -685,7 +727,9 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
||||
badConnection={false}
|
||||
canPost={false}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -734,7 +778,6 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -743,13 +786,18 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
@@ -781,7 +829,9 @@ exports[`components/advanced_create_post should match snapshot, init 1`] = `
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -830,7 +880,6 @@ exports[`components/advanced_create_post should match snapshot, init 1`] = `
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -839,13 +888,18 @@ exports[`components/advanced_create_post should match snapshot, init 1`] = `
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
@@ -877,7 +931,9 @@ exports[`components/advanced_create_post should match snapshot, post priority di
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -931,7 +987,6 @@ exports[`components/advanced_create_post should match snapshot, post priority di
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -940,13 +995,18 @@ exports[`components/advanced_create_post should match snapshot, post priority di
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
@@ -986,7 +1046,9 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -1035,7 +1097,6 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -1044,13 +1105,18 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
||||
handleUploadStart={[Function]}
|
||||
hideEmojiPicker={[Function]}
|
||||
isFormattingBarHidden={false}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
@@ -1095,7 +1161,9 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
||||
badConnection={false}
|
||||
canPost={true}
|
||||
canUploadFiles={false}
|
||||
caretPosition={0}
|
||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||
ctrlSend={false}
|
||||
currentChannel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
@@ -1149,7 +1217,6 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
||||
handleFileUploadChange={[Function]}
|
||||
handleFileUploadComplete={[Function]}
|
||||
handleGifClick={[Function]}
|
||||
handleKeyDown={[Function]}
|
||||
handleMouseUpKeyUp={[Function]}
|
||||
handlePostError={[Function]}
|
||||
handleSubmit={[Function]}
|
||||
@@ -1173,13 +1240,18 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
||||
}
|
||||
/>
|
||||
}
|
||||
loadNextMessage={[Function]}
|
||||
loadPrevMessage={[Function]}
|
||||
location="CENTER"
|
||||
maxPostSize={4000}
|
||||
message=""
|
||||
onEditLatestPost={[Function]}
|
||||
onMessageChange={[Function]}
|
||||
postId=""
|
||||
postMsgKeyPress={[Function]}
|
||||
prefillMessage={[Function]}
|
||||
removePreview={[Function]}
|
||||
replyToLastPost={[Function]}
|
||||
serverError={null}
|
||||
setShowPreview={[Function]}
|
||||
shouldShowPreview={false}
|
||||
|
||||
@@ -9,23 +9,16 @@ import type {CommandArgs} from '@mattermost/types/integrations';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
import {PostPriority} from '@mattermost/types/posts';
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions';
|
||||
|
||||
import type {Props} from 'components/advanced_create_post/advanced_create_post';
|
||||
import AdvancedCreatePost from 'components/advanced_create_post/advanced_create_post';
|
||||
import AdvanceTextEditor from 'components/advanced_text_editor/advanced_text_editor';
|
||||
import type {Props} from 'components/advanced_create_post/advanced_create_post';
|
||||
import type {TextboxElement} from 'components/textbox';
|
||||
|
||||
import {testComponentForLineBreak} from 'tests/helpers/line_break_helpers';
|
||||
import {testComponentForMarkdownHotkeys} from 'tests/helpers/markdown_hotkey_helpers.js';
|
||||
import Constants, {StoragePrefixes, ModalIdentifiers} from 'utils/constants';
|
||||
import EmojiMap from 'utils/emoji_map';
|
||||
import {execCommandInsertText} from 'utils/exec_commands';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
jest.mock('actions/global_actions', () => ({
|
||||
emitLocalUserTypingEvent: jest.fn(),
|
||||
@@ -993,171 +986,6 @@ describe('components/advanced_create_post', () => {
|
||||
expect(instance.handleFileUploadChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should just return as ctrlSend is enabled and its ctrl+enter', () => {
|
||||
const wrapper = shallow(advancedCreatePost({
|
||||
ctrlSend: true,
|
||||
}));
|
||||
|
||||
const instance: any = wrapper.instance();
|
||||
instance.textboxRef.current = {blur: jest.fn()};
|
||||
|
||||
const target = {
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
value: 'brown\nfox jumps over lazy dog',
|
||||
};
|
||||
|
||||
const event = {
|
||||
ctrlKey: true,
|
||||
key: Constants.KeyCodes.ENTER[0],
|
||||
keyCode: Constants.KeyCodes.ENTER[1],
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
persist: jest.fn(),
|
||||
target,
|
||||
} as unknown as React.KeyboardEvent<TextboxElement>;
|
||||
|
||||
instance.handleKeyDown(event);
|
||||
setTimeout(() => {
|
||||
expect(GlobalActions.emitLocalUserTypingEvent).toHaveBeenCalledWith(currentChannelProp.id, '');
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('Should call edit action as comment for arrow up', () => {
|
||||
const setEditingPost = jest.fn();
|
||||
const wrapper = shallow(advancedCreatePost({
|
||||
actions: {
|
||||
...baseProp.actions,
|
||||
setEditingPost,
|
||||
},
|
||||
}));
|
||||
const instance = wrapper.instance() as AdvancedCreatePost;
|
||||
const type = Utils.localizeMessage('create_post.comment', Posts.MESSAGE_TYPES.COMMENT);
|
||||
|
||||
const target = {
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
value: 'brown\nfox jumps over lazy dog',
|
||||
};
|
||||
|
||||
const event = {
|
||||
key: Constants.KeyCodes.UP[0],
|
||||
keyCode: Constants.KeyCodes.UP[1],
|
||||
preventDefault: jest.fn(),
|
||||
persist: jest.fn(),
|
||||
target,
|
||||
} as unknown as React.KeyboardEvent<TextboxElement>;
|
||||
|
||||
instance.handleKeyDown(event);
|
||||
expect(setEditingPost).toHaveBeenCalledWith(currentUsersLatestPostProp.id, 'post_textbox', type);
|
||||
});
|
||||
|
||||
it('Should call edit action as post for arrow up', () => {
|
||||
const setEditingPost = jest.fn();
|
||||
const wrapper = shallow(advancedCreatePost({
|
||||
actions: {
|
||||
...baseProp.actions,
|
||||
setEditingPost,
|
||||
},
|
||||
}));
|
||||
const instance = wrapper.instance() as AdvancedCreatePost;
|
||||
|
||||
wrapper.setProps({
|
||||
currentUsersLatestPost: {id: 'b', channel_id: currentChannelProp.id},
|
||||
});
|
||||
|
||||
const type = Utils.localizeMessage('create_post.post', Posts.MESSAGE_TYPES.POST);
|
||||
|
||||
const target = {
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
value: 'brown\nfox jumps over lazy dog',
|
||||
};
|
||||
|
||||
const event = {
|
||||
key: Constants.KeyCodes.UP[0],
|
||||
keyCode: Constants.KeyCodes.UP[1],
|
||||
preventDefault: jest.fn(),
|
||||
persist: jest.fn(),
|
||||
target,
|
||||
} as unknown as React.KeyboardEvent<TextboxElement>;
|
||||
|
||||
instance.handleKeyDown(event);
|
||||
expect(setEditingPost).toHaveBeenCalledWith(currentUsersLatestPostProp.id, 'post_textbox', type);
|
||||
});
|
||||
|
||||
it('Should call moveHistoryIndexForward as ctrlKey and down arrow', () => {
|
||||
const moveHistoryIndexForward = jest.fn(
|
||||
() => {
|
||||
return new Promise<void>((resolve) => {
|
||||
process.nextTick(() => resolve());
|
||||
});
|
||||
},
|
||||
);
|
||||
const wrapper = shallow(advancedCreatePost({
|
||||
actions: {
|
||||
...baseProp.actions,
|
||||
moveHistoryIndexForward,
|
||||
},
|
||||
}));
|
||||
const instance = wrapper.instance() as AdvancedCreatePost;
|
||||
|
||||
const target = {
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
value: 'brown\nfox jumps over lazy dog',
|
||||
};
|
||||
|
||||
const event = {
|
||||
ctrlKey: true,
|
||||
key: Constants.KeyCodes.DOWN[0],
|
||||
keyCode: Constants.KeyCodes.DOWN[1],
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
persist: jest.fn(),
|
||||
target,
|
||||
} as unknown as React.KeyboardEvent<TextboxElement>;
|
||||
|
||||
instance.handleKeyDown(event);
|
||||
expect(moveHistoryIndexForward).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should call moveHistoryIndexBack as ctrlKey and up arrow', () => {
|
||||
const moveHistoryIndexBack = jest.fn(
|
||||
() => {
|
||||
return new Promise<void>((resolve) => {
|
||||
process.nextTick(() => resolve());
|
||||
});
|
||||
},
|
||||
);
|
||||
const wrapper = shallow(advancedCreatePost({
|
||||
actions: {
|
||||
...baseProp.actions,
|
||||
moveHistoryIndexBack,
|
||||
},
|
||||
}));
|
||||
const instance = wrapper.instance() as AdvancedCreatePost;
|
||||
|
||||
const target = {
|
||||
selectionStart: 0,
|
||||
selectionEnd: 0,
|
||||
value: 'brown\nfox jumps over lazy dog',
|
||||
};
|
||||
|
||||
const event = {
|
||||
ctrlKey: true,
|
||||
key: Constants.KeyCodes.UP[0],
|
||||
keyCode: Constants.KeyCodes.UP[1],
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
persist: jest.fn(),
|
||||
target,
|
||||
} as unknown as React.KeyboardEvent<TextboxElement>;
|
||||
|
||||
instance.handleKeyDown(event);
|
||||
expect(moveHistoryIndexBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Show tutorial', () => {
|
||||
const wrapper = shallow(advancedCreatePost({
|
||||
showSendTutorialTip: true,
|
||||
@@ -1300,129 +1128,6 @@ describe('components/advanced_create_post', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to format a pasted markdown table', () => {
|
||||
const wrapper = shallow(advancedCreatePost());
|
||||
const mockImpl = () => {
|
||||
return {
|
||||
setSelectionRange: jest.fn(),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
};
|
||||
(wrapper.instance() as any).textboxRef.current = {getInputBox: jest.fn(mockImpl), focus: jest.fn(), blur: jest.fn()};
|
||||
|
||||
const event = {
|
||||
target: {
|
||||
id: 'post_textbox',
|
||||
},
|
||||
preventDefault: jest.fn(),
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/html'],
|
||||
getData: () => {
|
||||
return '<table><tr><th>test</th><th>test</th></tr><tr><td>test</td><td>test</td></tr></table>';
|
||||
},
|
||||
},
|
||||
} as unknown as ClipboardEvent;
|
||||
|
||||
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |';
|
||||
|
||||
(wrapper.instance() as AdvancedCreatePost).pasteHandler(event);
|
||||
expect(execCommandInsertText).toHaveBeenCalledWith(markdownTable);
|
||||
});
|
||||
|
||||
it('should be able to format a pasted markdown table without headers', () => {
|
||||
const wrapper = shallow(advancedCreatePost());
|
||||
const mockImpl = () => {
|
||||
return {
|
||||
setSelectionRange: jest.fn(),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
};
|
||||
(wrapper.instance() as any).textboxRef.current = {getInputBox: jest.fn(mockImpl), focus: jest.fn(), blur: jest.fn()};
|
||||
|
||||
const event = {
|
||||
target: {
|
||||
id: 'post_textbox',
|
||||
},
|
||||
preventDefault: jest.fn(),
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/html'],
|
||||
getData: () => {
|
||||
return '<table><tr><td>test</td><td>test</td></tr><tr><td>test</td><td>test</td></tr></table>';
|
||||
},
|
||||
},
|
||||
} as unknown as ClipboardEvent;
|
||||
|
||||
const markdownTable = '| test | test |\n| --- | --- |\n| test | test |\n';
|
||||
|
||||
(wrapper.instance() as AdvancedCreatePost).pasteHandler(event);
|
||||
expect(execCommandInsertText).toHaveBeenCalledWith(markdownTable);
|
||||
});
|
||||
|
||||
it('should be able to format a pasted hyperlink', () => {
|
||||
const wrapper = shallow(advancedCreatePost());
|
||||
const mockImpl = () => {
|
||||
return {
|
||||
setSelectionRange: jest.fn(),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
};
|
||||
(wrapper.instance() as any).textboxRef.current = {getInputBox: jest.fn(mockImpl), focus: jest.fn(), blur: jest.fn()};
|
||||
|
||||
const event = {
|
||||
target: {
|
||||
id: 'post_textbox',
|
||||
},
|
||||
preventDefault: jest.fn(),
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/html'],
|
||||
getData: () => {
|
||||
return '<a href="https://test.domain">link text</a>';
|
||||
},
|
||||
},
|
||||
} as unknown as ClipboardEvent;
|
||||
|
||||
const markdownLink = '[link text](https://test.domain)';
|
||||
|
||||
(wrapper.instance() as AdvancedCreatePost).pasteHandler(event);
|
||||
expect(execCommandInsertText).toHaveBeenCalledWith(markdownLink);
|
||||
});
|
||||
|
||||
it('should be able to format a github codeblock (pasted as a table)', () => {
|
||||
const wrapper = shallow(advancedCreatePost());
|
||||
const mockImpl = () => {
|
||||
return {
|
||||
setSelectionRange: jest.fn(),
|
||||
focus: jest.fn(),
|
||||
};
|
||||
};
|
||||
(wrapper.instance() as any).textboxRef.current = {getInputBox: jest.fn(mockImpl), focus: jest.fn(), blur: jest.fn()};
|
||||
|
||||
const event = {
|
||||
target: {
|
||||
id: 'post_textbox',
|
||||
},
|
||||
preventDefault: jest.fn(),
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/plain', 'text/html'],
|
||||
getData: (type: string) => {
|
||||
if (type === 'text/plain') {
|
||||
return '// a javascript codeblock example\nif (1 > 0) {\n return \'condition is true\';\n}';
|
||||
}
|
||||
return '<table class="highlight tab-size js-file-line-container" data-tab-size="8"><tbody><tr><td id="LC1" class="blob-code blob-code-inner js-file-line"><span class="pl-c"><span class="pl-c">//</span> a javascript codeblock example</span></td></tr><tr><td id="L2" class="blob-num js-line-number" data-line-number="2"> </td><td id="LC2" class="blob-code blob-code-inner js-file-line"><span class="pl-k">if</span> (<span class="pl-c1">1</span> <span class="pl-k">></span> <span class="pl-c1">0</span>) {</td></tr><tr><td id="L3" class="blob-num js-line-number" data-line-number="3"> </td><td id="LC3" class="blob-code blob-code-inner js-file-line"><span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span class="pl-pds">\'</span>condition is true<span class="pl-pds">\'</span></span>);</td></tr><tr><td id="L4" class="blob-num js-line-number" data-line-number="4"> </td><td id="LC4" class="blob-code blob-code-inner js-file-line">}</td></tr></tbody></table>';
|
||||
},
|
||||
},
|
||||
} as unknown as ClipboardEvent;
|
||||
|
||||
const codeBlockMarkdown = "```\n// a javascript codeblock example\nif (1 > 0) {\n return 'condition is true';\n}\n```";
|
||||
|
||||
(wrapper.instance() as AdvancedCreatePost).pasteHandler(event);
|
||||
expect(execCommandInsertText).toHaveBeenCalledWith(codeBlockMarkdown);
|
||||
});
|
||||
|
||||
/**
|
||||
* TODO@all: move this test to advanced_text_editor.test.tsx and rewrite it according to the component
|
||||
*
|
||||
@@ -1471,26 +1176,6 @@ describe('components/advanced_create_post', () => {
|
||||
false,
|
||||
);
|
||||
|
||||
testComponentForMarkdownHotkeys(
|
||||
(value: string) => advancedCreatePost({draft: {...draftProp, message: value}}),
|
||||
(wrapper: any, setSelectionRangeFn: any) => {
|
||||
wrapper.instance().textboxRef = {
|
||||
current: {
|
||||
getInputBox: jest.fn(() => {
|
||||
return {
|
||||
focus: jest.fn(),
|
||||
setSelectionRange: setSelectionRangeFn,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
(instance: any) => instance.find(AdvanceTextEditor),
|
||||
(instance: any) => instance.state().message,
|
||||
false,
|
||||
'post_textbox',
|
||||
);
|
||||
|
||||
it('should match snapshot, can post; preview enabled', () => {
|
||||
const wrapper = shallow(advancedCreatePost({canPost: true}));
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import {isNil} from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import type {Channel, ChannelMemberCountsByGroup} from '@mattermost/types/channels';
|
||||
@@ -44,19 +43,9 @@ import Constants, {
|
||||
AdvancedTextEditor as AdvancedTextEditorConst,
|
||||
} from 'utils/constants';
|
||||
import type EmojiMap from 'utils/emoji_map';
|
||||
import {execCommandInsertText} from 'utils/exec_commands';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import {applyMarkdown} from 'utils/markdown/apply_markdown';
|
||||
import type {ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
||||
import {
|
||||
getHtmlTable,
|
||||
hasHtmlLink,
|
||||
formatMarkdownMessage,
|
||||
formatGithubCodePaste,
|
||||
isGitHubCodeBlock,
|
||||
formatMarkdownLinkMessage,
|
||||
isTextUrl,
|
||||
} from 'utils/paste';
|
||||
import {
|
||||
containsAtChannel,
|
||||
specialMentionsInText,
|
||||
@@ -67,7 +56,6 @@ import {
|
||||
groupsMentionedInText,
|
||||
mentionsMinusSpecialMentionsInText,
|
||||
hasRequestedPersistentNotifications,
|
||||
isWithinCodeBlock,
|
||||
} from 'utils/post_utils';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import * as Utils from 'utils/utils';
|
||||
@@ -325,7 +313,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
actions.setShowPreview(false);
|
||||
actions.clearDraftUploads();
|
||||
this.focusTextbox();
|
||||
document.addEventListener('paste', this.pasteHandler);
|
||||
document.addEventListener('keydown', this.documentKeyHandler);
|
||||
window.addEventListener('beforeunload', this.unloadHandler);
|
||||
this.setOrientationListeners();
|
||||
@@ -357,7 +344,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('paste', this.pasteHandler);
|
||||
document.removeEventListener('keydown', this.documentKeyHandler);
|
||||
window.removeEventListener('beforeunload', this.unloadHandler);
|
||||
this.removeOrientationListeners();
|
||||
@@ -928,43 +914,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
this.draftsForChannel[channelId] = draft;
|
||||
};
|
||||
|
||||
pasteHandler = (event: ClipboardEvent) => {
|
||||
const {clipboardData, target} = event;
|
||||
|
||||
if (!clipboardData || !clipboardData.items || !target || ((target as TextboxElement)?.id !== 'post_textbox')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {selectionStart, selectionEnd} = target as TextboxElement;
|
||||
|
||||
const hasSelection = !isNil(selectionStart) && !isNil(selectionEnd) && selectionStart < selectionEnd;
|
||||
const hasTextUrl = isTextUrl(clipboardData);
|
||||
const hasHTMLLinks = !this.isNonFormattedPaste && hasHtmlLink(clipboardData);
|
||||
const htmlTable = getHtmlTable(clipboardData);
|
||||
const shouldApplyLinkMarkdown = hasSelection && hasTextUrl;
|
||||
const shouldApplyGithubCodeBlock = htmlTable && isGitHubCodeBlock(htmlTable.className);
|
||||
|
||||
if (!htmlTable && !hasHTMLLinks && !shouldApplyLinkMarkdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const message = this.state.message;
|
||||
|
||||
// execCommand's insertText' triggers a 'change' event, hence we need not set respective state explicitly.
|
||||
if (shouldApplyLinkMarkdown) {
|
||||
const formattedLink = formatMarkdownLinkMessage({selectionStart, selectionEnd, message, clipboardData});
|
||||
execCommandInsertText(formattedLink);
|
||||
} else if (shouldApplyGithubCodeBlock) {
|
||||
const {formattedCodeBlock} = formatGithubCodePaste({selectionStart, selectionEnd, message, clipboardData});
|
||||
execCommandInsertText(formattedCodeBlock);
|
||||
} else {
|
||||
const {formattedMarkdown} = formatMarkdownMessage(clipboardData, message, this.state.caretPosition);
|
||||
execCommandInsertText(formattedMarkdown);
|
||||
}
|
||||
};
|
||||
|
||||
handleFileUploadChange = () => {
|
||||
this.focusTextbox();
|
||||
};
|
||||
@@ -1128,11 +1077,9 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
|
||||
fillMessageFromHistory() {
|
||||
const lastMessage = this.props.messageInHistoryItem;
|
||||
if (lastMessage) {
|
||||
this.setState({
|
||||
message: lastMessage,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
message: lastMessage || '',
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseUpKeyUp = (e: React.MouseEvent | React.KeyboardEvent) => {
|
||||
@@ -1141,194 +1088,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
handleKeyDown = (e: React.KeyboardEvent<TextboxElement>) => {
|
||||
const messageIsEmpty = this.state.message.length === 0;
|
||||
const draftMessageIsEmpty = this.props.draft.message.length === 0;
|
||||
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||
|
||||
// fix for FF not capturing the paste without formatting event when using ctrl|cmd + shift + v
|
||||
if (e.key === KeyCodes.V[0] && ctrlOrMetaKeyPressed) {
|
||||
if (e.shiftKey) {
|
||||
this.isNonFormattedPaste = true;
|
||||
this.timeoutId = window.setTimeout(() => {
|
||||
this.isNonFormattedPaste = false;
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
|
||||
const ctrlEnterKeyCombo = (this.props.ctrlSend || this.props.codeBlockOnCtrlEnter) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
ctrlOrMetaKeyPressed;
|
||||
|
||||
const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const shiftAltCombo = !Keyboard.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||
|
||||
// listen for line break key combo and insert new line character
|
||||
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||
this.setState({message: Utils.insertLineBreakFromKeyEvent(e)});
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctrlEnterKeyCombo) {
|
||||
this.setShowPreview(false);
|
||||
this.postMsgKeyPress(e);
|
||||
return;
|
||||
}
|
||||
|
||||
const {message, caretPosition} = this.state;
|
||||
const caretIsWithinCodeBlock = isWithinCodeBlock(message, caretPosition);
|
||||
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||
this.textboxRef.current?.blur();
|
||||
}
|
||||
|
||||
if (
|
||||
!e.ctrlKey &&
|
||||
!e.metaKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.UP) &&
|
||||
message === ''
|
||||
) {
|
||||
e.preventDefault();
|
||||
if (this.textboxRef.current) {
|
||||
this.textboxRef.current.blur();
|
||||
}
|
||||
|
||||
this.editLastPost(e);
|
||||
}
|
||||
|
||||
const {
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
value,
|
||||
} = e.target as TextboxElement;
|
||||
|
||||
if (ctrlKeyCombo && !caretIsWithinCodeBlock) {
|
||||
if (draftMessageIsEmpty && Keyboard.isKeyPressed(e, KeyCodes.UP)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.loadPrevMessage(e);
|
||||
} else if (draftMessageIsEmpty && Keyboard.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.loadNextMessage(e);
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.B)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'bold',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.I)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'italic',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isTextSelectedInPostOrReply(e) && Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'link',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
}
|
||||
} else if (ctrlAltCombo && !caretIsWithinCodeBlock) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'link',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.C)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'code',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleEmojiPicker();
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.T)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleAdvanceTextEditor();
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.P) && message.length) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.setShowPreview(!this.props.shouldShowPreview);
|
||||
}
|
||||
} else if (shiftAltCombo && !caretIsWithinCodeBlock) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.X)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'strike',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ol',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'ul',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.NINE)) {
|
||||
e.preventDefault();
|
||||
this.applyMarkdown({
|
||||
markdownMode: 'quote',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
const upKeyOnly = !ctrlOrMetaKeyPressed && !e.altKey && !e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
|
||||
const shiftUpKeyCombo = !ctrlOrMetaKeyPressed && !e.altKey && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
|
||||
const ctrlShiftCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.shiftKey;
|
||||
|
||||
if (upKeyOnly && messageIsEmpty) {
|
||||
this.editLastPost(e);
|
||||
} else if (shiftUpKeyCombo && messageIsEmpty) {
|
||||
this.replyToLastPost(e);
|
||||
} else if (ctrlShiftCombo && Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.toggleEmojiPicker();
|
||||
} else if (((UserAgent.isMac() && ctrlShiftCombo) || (!UserAgent.isMac() && ctrlAltCombo)) && Keyboard.isKeyPressed(e, KeyCodes.P) && this.state.message.length) {
|
||||
this.setShowPreview(!this.props.shouldShowPreview);
|
||||
} else if (ctrlAltCombo && Keyboard.isKeyPressed(e, KeyCodes.T)) {
|
||||
this.toggleAdvanceTextEditor();
|
||||
}
|
||||
};
|
||||
|
||||
editLastPost = (e: React.KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1580,6 +1339,14 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
return Object.values(this.getSpecialMentions()).includes(true);
|
||||
};
|
||||
|
||||
onMessageChange = (message: string, callback?: (() => void) | undefined) => {
|
||||
this.handleDraftChange({
|
||||
...this.props.draft,
|
||||
message,
|
||||
});
|
||||
this.setState({message}, callback);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {draft, canPost} = this.props;
|
||||
|
||||
@@ -1666,7 +1433,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
handlePostError={this.handlePostError}
|
||||
emitTypingEvent={this.emitTypingEvent}
|
||||
handleMouseUpKeyUp={this.handleMouseUpKeyUp}
|
||||
handleKeyDown={this.handleKeyDown}
|
||||
postMsgKeyPress={this.postMsgKeyPress}
|
||||
handleChange={this.handleChange}
|
||||
toggleEmojiPicker={this.toggleEmojiPicker}
|
||||
@@ -1707,6 +1473,14 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
||||
),
|
||||
...(pluginItems || []),
|
||||
].filter(Boolean)}
|
||||
codeBlockOnCtrlEnter={this.props.codeBlockOnCtrlEnter}
|
||||
ctrlSend={this.props.ctrlSend}
|
||||
loadNextMessage={this.loadNextMessage}
|
||||
loadPrevMessage={this.loadPrevMessage}
|
||||
onEditLatestPost={this.editLastPost}
|
||||
onMessageChange={this.onMessageChange}
|
||||
replyToLastPost={this.replyToLastPost}
|
||||
caretPosition={this.state.caretPosition}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -44,7 +44,7 @@ import {searchAssociatedGroupsForReference} from 'actions/views/group';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {selectPostFromRightHandSideSearchByPostId} from 'actions/views/rhs';
|
||||
import {setShowPreviewOnCreatePost} from 'actions/views/textbox';
|
||||
import {getEmojiMap, getShortcutReactToLastPostEmittedFrom} from 'selectors/emojis';
|
||||
import {getEmojiMap} from 'selectors/emojis';
|
||||
import {getCurrentLocale} from 'selectors/i18n';
|
||||
import {makeGetChannelDraft, getIsRhsExpanded, getIsRhsOpen} from 'selectors/rhs';
|
||||
import {connectionErrorCount} from 'selectors/views/system';
|
||||
@@ -81,7 +81,6 @@ function makeMapStateToProps() {
|
||||
const userIsOutOfOffice = getStatusForUserId(state, currentUserId) === UserStatuses.OUT_OF_OFFICE;
|
||||
const badConnection = connectionErrorCount(state) > 1;
|
||||
const isTimezoneEnabled = config.ExperimentalTimezone === 'true';
|
||||
const shortcutReactToLastPostEmittedFrom = getShortcutReactToLastPostEmittedFrom(state);
|
||||
const canPost = haveICurrentChannelPermission(state, Permissions.CREATE_POST);
|
||||
const useChannelMentions = haveICurrentChannelPermission(state, Permissions.USE_CHANNEL_MENTIONS);
|
||||
const isLDAPEnabled = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
|
||||
@@ -128,7 +127,6 @@ function makeMapStateToProps() {
|
||||
emojiMap: getEmojiMap(state),
|
||||
badConnection,
|
||||
isTimezoneEnabled,
|
||||
shortcutReactToLastPostEmittedFrom,
|
||||
canPost,
|
||||
useChannelMentions,
|
||||
shouldShowPreview: showPreviewOnCreatePost(state),
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import Permissions from 'mattermost-redux/constants/permissions';
|
||||
|
||||
import type {FileUpload} from 'components/file_upload/file_upload';
|
||||
import type Textbox from 'components/textbox/textbox';
|
||||
|
||||
import WebSocketClient from 'client/web_websocket_client';
|
||||
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
|
||||
import {renderWithFullContext, userEvent} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
import {WebSocketContext} from 'utils/use_websocket';
|
||||
|
||||
import type {PostDraft} from 'types/store/draft';
|
||||
|
||||
import AdavancedTextEditor from './advanced_text_editor';
|
||||
|
||||
global.ResizeObserver = require('resize-observer-polyfill');
|
||||
|
||||
const currentUserId = 'current_user_id';
|
||||
const channelId = 'current_channel_id';
|
||||
|
||||
const initialState = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableConfirmNotificationsToChannel: 'false',
|
||||
EnableCustomGroups: 'false',
|
||||
PostPriority: 'false',
|
||||
ExperimentalTimezone: 'false',
|
||||
EnableCustomEmoji: 'false',
|
||||
AllowSyncedDrafts: 'false',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'false',
|
||||
LDAPGroups: 'false',
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
channels: {
|
||||
current_channel_id: TestHelper.getChannelMock({id: 'current_channel_id', team_id: 'current_team_id'}),
|
||||
},
|
||||
stats: {
|
||||
current_channel_id: {
|
||||
member_count: 1,
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
current_channel_id: new Set(['channel_roles']),
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'current_team_id',
|
||||
teams: {
|
||||
current_team_id: TestHelper.getTeamMock({id: 'current_team_id'}),
|
||||
},
|
||||
myMembers: {
|
||||
current_team_id: TestHelper.getTeamMembershipMock({roles: 'team_roles'}),
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: TestHelper.getUserMock({id: 'current_user_id', roles: 'user_roles'}),
|
||||
},
|
||||
statuses: {
|
||||
current_user_id: 'online',
|
||||
},
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
connectionId: 'connection_id',
|
||||
},
|
||||
};
|
||||
|
||||
const emptyDraft: PostDraft = {
|
||||
message: '',
|
||||
uploadsInProgress: [],
|
||||
fileInfos: [],
|
||||
channelId,
|
||||
rootId: '',
|
||||
createAt: 0,
|
||||
updateAt: 0,
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
location: 'CENTER',
|
||||
message: '',
|
||||
showEmojiPicker: false,
|
||||
uploadsProgressPercent: {},
|
||||
currentChannel: initialState.entities.channels.channels.current_channel_id as Channel,
|
||||
channelId,
|
||||
postId: '',
|
||||
errorClass: null,
|
||||
serverError: null,
|
||||
postError: null,
|
||||
isFormattingBarHidden: false,
|
||||
draft: emptyDraft,
|
||||
badConnection: false,
|
||||
handleSubmit: jest.fn(),
|
||||
removePreview: jest.fn(),
|
||||
showSendTutorialTip: false,
|
||||
setShowPreview: jest.fn(),
|
||||
shouldShowPreview: false,
|
||||
maxPostSize: 100,
|
||||
canPost: true,
|
||||
applyMarkdown: jest.fn(),
|
||||
useChannelMentions: false,
|
||||
currentChannelTeammateUsername: '',
|
||||
currentUserId,
|
||||
canUploadFiles: true,
|
||||
enableEmojiPicker: true,
|
||||
enableGifPicker: true,
|
||||
handleBlur: jest.fn(),
|
||||
handlePostError: jest.fn(),
|
||||
emitTypingEvent: jest.fn(),
|
||||
handleMouseUpKeyUp: jest.fn(),
|
||||
postMsgKeyPress: jest.fn(),
|
||||
handleChange: jest.fn(),
|
||||
toggleEmojiPicker: jest.fn(),
|
||||
handleGifClick: jest.fn(),
|
||||
handleEmojiClick: jest.fn(),
|
||||
hideEmojiPicker: jest.fn(),
|
||||
toggleAdvanceTextEditor: jest.fn(),
|
||||
handleUploadProgress: jest.fn(),
|
||||
handleUploadError: jest.fn(),
|
||||
handleFileUploadComplete: jest.fn(),
|
||||
handleUploadStart: jest.fn(),
|
||||
handleFileUploadChange: jest.fn(),
|
||||
getFileUploadTarget: jest.fn(),
|
||||
fileUploadRef: React.createRef<FileUpload>(),
|
||||
prefillMessage: jest.fn(),
|
||||
textboxRef: React.createRef<Textbox>(),
|
||||
isThreadView: false,
|
||||
ctrlSend: true,
|
||||
codeBlockOnCtrlEnter: true,
|
||||
onMessageChange: jest.fn(),
|
||||
onEditLatestPost: jest.fn(),
|
||||
loadPrevMessage: jest.fn(),
|
||||
loadNextMessage: jest.fn(),
|
||||
replyToLastPost: jest.fn(),
|
||||
caretPosition: 0,
|
||||
};
|
||||
|
||||
describe('components/avanced_text_editor/advanced_text_editor', () => {
|
||||
describe('keyDown behavior', () => {
|
||||
it('Enter should call postMsgKeyPress', () => {
|
||||
const postMsgKeyPress = jest.fn();
|
||||
renderWithFullContext(
|
||||
<WebSocketContext.Provider value={WebSocketClient}>
|
||||
<AdavancedTextEditor
|
||||
{...baseProps}
|
||||
postMsgKeyPress={postMsgKeyPress}
|
||||
message={'test'}
|
||||
/>
|
||||
</WebSocketContext.Provider>, mergeObjects(initialState, {
|
||||
entities: {
|
||||
roles: {
|
||||
roles: {
|
||||
user_roles: {permissions: [Permissions.CREATE_POST]},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
userEvent.type(screen.getByTestId('post_textbox'), '{enter}');
|
||||
expect(postMsgKeyPress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Ctrl+up should call loadPrevMessage', () => {
|
||||
const loadPrevMessage = jest.fn();
|
||||
renderWithFullContext(
|
||||
<WebSocketContext.Provider value={WebSocketClient}>
|
||||
<AdavancedTextEditor
|
||||
{...baseProps}
|
||||
loadPrevMessage={loadPrevMessage}
|
||||
/>
|
||||
</WebSocketContext.Provider>, mergeObjects(initialState, {
|
||||
entities: {
|
||||
roles: {
|
||||
roles: {
|
||||
user_roles: {permissions: [Permissions.CREATE_POST]},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
userEvent.type(screen.getByTestId('post_textbox'), '{ctrl}{arrowup}');
|
||||
expect(loadPrevMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('up should call onEditLatestPost', () => {
|
||||
const onEditLatestPost = jest.fn();
|
||||
renderWithFullContext(
|
||||
<WebSocketContext.Provider value={WebSocketClient}>
|
||||
<AdavancedTextEditor
|
||||
{...baseProps}
|
||||
onEditLatestPost={onEditLatestPost}
|
||||
/>
|
||||
</WebSocketContext.Provider>, mergeObjects(initialState, {
|
||||
entities: {
|
||||
roles: {
|
||||
roles: {
|
||||
user_roles: {permissions: [Permissions.CREATE_POST]},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
userEvent.type(screen.getByTestId('post_textbox'), '{arrowup}');
|
||||
expect(onEditLatestPost).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('ESC should blur the input', () => {
|
||||
renderWithFullContext(
|
||||
<WebSocketContext.Provider value={WebSocketClient}>
|
||||
<AdavancedTextEditor
|
||||
{...baseProps}
|
||||
/>
|
||||
</WebSocketContext.Provider>, mergeObjects(initialState, {
|
||||
entities: {
|
||||
roles: {
|
||||
roles: {
|
||||
user_roles: {permissions: [Permissions.CREATE_POST]},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
const textbox = screen.getByTestId('post_textbox');
|
||||
userEvent.type(textbox, 'something{esc}');
|
||||
expect(textbox).not.toHaveFocus();
|
||||
});
|
||||
|
||||
describe('markdown', () => {
|
||||
const ttcc = [
|
||||
{
|
||||
input: '{ctrl}b',
|
||||
markdownMode: 'bold',
|
||||
},
|
||||
{
|
||||
input: '{ctrl}i',
|
||||
markdownMode: 'italic',
|
||||
},
|
||||
{
|
||||
input: '{ctrl}k',
|
||||
markdownMode: 'link',
|
||||
},
|
||||
{
|
||||
input: '{ctrl}{alt}k',
|
||||
markdownMode: 'link',
|
||||
},
|
||||
];
|
||||
for (const tc of ttcc) {
|
||||
it(`component adds ${tc.markdownMode} markdown`, () => {
|
||||
const applyMarkdown = jest.fn();
|
||||
const message = 'Some markdown text';
|
||||
const selectionStart = 5;
|
||||
const selectionEnd = 10;
|
||||
|
||||
renderWithFullContext(
|
||||
<WebSocketContext.Provider value={WebSocketClient}>
|
||||
<AdavancedTextEditor
|
||||
{...baseProps}
|
||||
applyMarkdown={applyMarkdown}
|
||||
message={'Some markdown text'}
|
||||
/>
|
||||
</WebSocketContext.Provider>, mergeObjects(initialState, {
|
||||
entities: {
|
||||
roles: {
|
||||
roles: {
|
||||
user_roles: {permissions: [Permissions.CREATE_POST]},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
const textbox = screen.getByTestId('post_textbox');
|
||||
userEvent.type(textbox, tc.input, {initialSelectionStart: selectionStart, initialSelectionEnd: selectionEnd});
|
||||
expect(applyMarkdown).toHaveBeenCalledWith({
|
||||
markdownMode: tc.markdownMode,
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
import classNames from 'classnames';
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {EmoticonHappyOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
@@ -11,6 +12,8 @@ import type {Emoji} from '@mattermost/types/emojis';
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {FileInfo} from '@mattermost/types/files';
|
||||
|
||||
import {emitShortcutReactToLastPostFrom} from 'actions/post_actions';
|
||||
|
||||
import AutoHeightSwitcher from 'components/common/auto_height_switcher';
|
||||
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
|
||||
import FilePreview from 'components/file_preview';
|
||||
@@ -29,7 +32,11 @@ import Tooltip from 'components/tooltip';
|
||||
import {SendMessageTour} from 'components/tours/onboarding_tour';
|
||||
|
||||
import Constants, {Locations} from 'utils/constants';
|
||||
import * as Keyboard from 'utils/keyboard';
|
||||
import type {ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
||||
import {pasteHandler} from 'utils/paste';
|
||||
import {isWithinCodeBlock} from 'utils/post_utils';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
import type {PostDraft} from 'types/store/draft';
|
||||
@@ -44,6 +51,8 @@ import ToggleFormattingBar from './toggle_formatting_bar';
|
||||
|
||||
import './advanced_text_editor.scss';
|
||||
|
||||
const KeyCodes = Constants.KeyCodes;
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
@@ -78,7 +87,6 @@ type Props = {
|
||||
handlePostError: (postError: React.ReactNode) => void;
|
||||
emitTypingEvent: () => void;
|
||||
handleMouseUpKeyUp: (e: React.MouseEvent<TextboxElement> | React.KeyboardEvent<TextboxElement>) => void;
|
||||
handleKeyDown: (e: React.KeyboardEvent<TextboxElement>) => void;
|
||||
postMsgKeyPress: (e: React.KeyboardEvent<TextboxElement>) => void;
|
||||
handleChange: (e: React.ChangeEvent<TextboxElement>) => void;
|
||||
toggleEmojiPicker: () => void;
|
||||
@@ -101,6 +109,14 @@ type Props = {
|
||||
additionalControls?: React.ReactNodeArray;
|
||||
labels?: React.ReactNode;
|
||||
disableSend?: boolean;
|
||||
ctrlSend?: boolean;
|
||||
codeBlockOnCtrlEnter?: boolean;
|
||||
onMessageChange: (message: string, callback?: () => void) => void;
|
||||
onEditLatestPost: (e: React.KeyboardEvent) => void;
|
||||
loadPrevMessage: (e: React.KeyboardEvent) => void;
|
||||
loadNextMessage: (e: React.KeyboardEvent) => void;
|
||||
replyToLastPost?: (e: React.KeyboardEvent) => void;
|
||||
caretPosition: number;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
@@ -136,7 +152,6 @@ const AdvanceTextEditor = ({
|
||||
handlePostError,
|
||||
emitTypingEvent,
|
||||
handleMouseUpKeyUp,
|
||||
handleKeyDown,
|
||||
postMsgKeyPress,
|
||||
handleChange,
|
||||
toggleEmojiPicker,
|
||||
@@ -157,6 +172,14 @@ const AdvanceTextEditor = ({
|
||||
additionalControls,
|
||||
labels,
|
||||
disableSend = false,
|
||||
ctrlSend,
|
||||
codeBlockOnCtrlEnter,
|
||||
onMessageChange,
|
||||
onEditLatestPost,
|
||||
loadPrevMessage,
|
||||
loadNextMessage,
|
||||
replyToLastPost,
|
||||
caretPosition,
|
||||
placeholder,
|
||||
}: Props) => {
|
||||
const readOnlyChannel = !canPost;
|
||||
@@ -173,6 +196,11 @@ const AdvanceTextEditor = ({
|
||||
const [showFormattingSpacer, setShowFormattingSpacer] = useState(shouldShowPreview);
|
||||
const [keepEditorInFocus, setKeepEditorInFocus] = useState(false);
|
||||
|
||||
const isNonFormattedPaste = useRef(false);
|
||||
const timeoutId = useRef<number>();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const input = textboxRef.current?.getInputBox();
|
||||
|
||||
const handleHeightChange = useCallback((height: number, maxHeight: number) => {
|
||||
@@ -192,6 +220,8 @@ const AdvanceTextEditor = ({
|
||||
setKeepEditorInFocus(true);
|
||||
}, []);
|
||||
|
||||
const isRHS = location === Locations.RHS_COMMENT;
|
||||
|
||||
let attachmentPreview = null;
|
||||
if (!readOnlyChannel && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0)) {
|
||||
attachmentPreview = (
|
||||
@@ -370,6 +400,205 @@ const AdvanceTextEditor = ({
|
||||
}
|
||||
}, [message, input]);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<TextboxElement>) => {
|
||||
const ctrlOrMetaKeyPressed = e.ctrlKey || e.metaKey;
|
||||
const ctrlEnterKeyCombo = (ctrlSend || codeBlockOnCtrlEnter) &&
|
||||
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
ctrlOrMetaKeyPressed;
|
||||
|
||||
const ctrlKeyCombo = Keyboard.cmdOrCtrlPressed(e) && !e.altKey && !e.shiftKey;
|
||||
const ctrlAltCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.altKey;
|
||||
const shiftAltCombo = !Keyboard.cmdOrCtrlPressed(e) && e.shiftKey && e.altKey;
|
||||
const ctrlShiftCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.shiftKey;
|
||||
|
||||
// fix for FF not capturing the paste without formatting event when using ctrl|cmd + shift + v
|
||||
if (e.key === KeyCodes.V[0] && ctrlOrMetaKeyPressed) {
|
||||
if (e.shiftKey) {
|
||||
isNonFormattedPaste.current = true;
|
||||
timeoutId.current = window.setTimeout(() => {
|
||||
isNonFormattedPaste.current = false;
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
|
||||
// listen for line break key combo and insert new line character
|
||||
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||
onMessageChange(Utils.insertLineBreakFromKeyEvent(e));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctrlEnterKeyCombo) {
|
||||
setShowPreview(false);
|
||||
postMsgKeyPress(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.ESCAPE)) {
|
||||
textboxRef.current?.blur();
|
||||
}
|
||||
|
||||
const upKeyOnly = !ctrlOrMetaKeyPressed && !e.altKey && !e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
|
||||
const messageIsEmpty = message.length === 0;
|
||||
const draftMessageIsEmpty = draft.message.length === 0;
|
||||
const caretIsWithinCodeBlock = caretPosition && isWithinCodeBlock(message, caretPosition);
|
||||
|
||||
if (upKeyOnly && messageIsEmpty) {
|
||||
e.preventDefault();
|
||||
if (textboxRef.current) {
|
||||
textboxRef.current.blur();
|
||||
}
|
||||
|
||||
onEditLatestPost(e);
|
||||
}
|
||||
|
||||
const {
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
value,
|
||||
} = e.target as TextboxElement;
|
||||
|
||||
if (ctrlKeyCombo && !caretIsWithinCodeBlock) {
|
||||
if (draftMessageIsEmpty && Keyboard.isKeyPressed(e, KeyCodes.UP)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
loadPrevMessage(e);
|
||||
} else if (draftMessageIsEmpty && Keyboard.isKeyPressed(e, KeyCodes.DOWN)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
loadNextMessage(e);
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.B)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
applyMarkdown({
|
||||
markdownMode: 'bold',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.I)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
applyMarkdown({
|
||||
markdownMode: 'italic',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Utils.isTextSelectedInPostOrReply(e) && Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
applyMarkdown({
|
||||
markdownMode: 'link',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
}
|
||||
} else if (ctrlAltCombo && !caretIsWithinCodeBlock) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.K)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
applyMarkdown({
|
||||
markdownMode: 'link',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.C)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
applyMarkdown({
|
||||
markdownMode: 'code',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleEmojiPicker();
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.T)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleAdvanceTextEditor();
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.P) && message.length && !UserAgent.isMac()) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowPreview(!shouldShowPreview);
|
||||
}
|
||||
} else if (shiftAltCombo && !caretIsWithinCodeBlock) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.X)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
applyMarkdown({
|
||||
markdownMode: 'strike',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.SEVEN)) {
|
||||
e.preventDefault();
|
||||
applyMarkdown({
|
||||
markdownMode: 'ol',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.EIGHT)) {
|
||||
e.preventDefault();
|
||||
applyMarkdown({
|
||||
markdownMode: 'ul',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.NINE)) {
|
||||
e.preventDefault();
|
||||
applyMarkdown({
|
||||
markdownMode: 'quote',
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
message: value,
|
||||
});
|
||||
}
|
||||
} else if (ctrlShiftCombo && !caretIsWithinCodeBlock) {
|
||||
if (Keyboard.isKeyPressed(e, KeyCodes.P) && message.length && UserAgent.isMac()) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowPreview(!shouldShowPreview);
|
||||
} else if (Keyboard.isKeyPressed(e, KeyCodes.E)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleEmojiPicker();
|
||||
}
|
||||
}
|
||||
|
||||
if (isRHS) {
|
||||
const lastMessageReactionKeyCombo = ctrlShiftCombo && Keyboard.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||
if (lastMessageReactionKeyCombo) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
dispatch(emitShortcutReactToLastPostFrom(Locations.RHS_ROOT));
|
||||
}
|
||||
} else {
|
||||
const shiftUpKeyCombo = !ctrlOrMetaKeyPressed && !e.altKey && e.shiftKey && Keyboard.isKeyPressed(e, KeyCodes.UP);
|
||||
if (shiftUpKeyCombo && messageIsEmpty) {
|
||||
replyToLastPost?.(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function onPaste(event: ClipboardEvent) {
|
||||
pasteHandler(event, location, message, isNonFormattedPaste.current, caretPosition);
|
||||
}
|
||||
|
||||
document.addEventListener('paste', onPaste);
|
||||
return () => {
|
||||
document.removeEventListener('paste', onPaste);
|
||||
};
|
||||
}, [location, message, caretPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!message) {
|
||||
handleWidthChange(0);
|
||||
|
||||
@@ -326,6 +326,8 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
|
||||
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||
ctrlOrMetaKeyPressed;
|
||||
const markdownLinkKey = Keyboard.isKeyPressed(e, KeyCodes.K);
|
||||
const ctrlShiftCombo = Keyboard.cmdOrCtrlPressed(e, true) && e.shiftKey;
|
||||
const lastMessageReactionKeyCombo = ctrlShiftCombo && Keyboard.isKeyPressed(e, KeyCodes.BACK_SLASH);
|
||||
|
||||
// listen for line break key combo and insert new line character
|
||||
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||
@@ -356,6 +358,10 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
|
||||
selectionEnd: e.currentTarget.selectionEnd,
|
||||
message: e.currentTarget.value,
|
||||
});
|
||||
} else if (lastMessageReactionKeyCombo) {
|
||||
// Stop document from handling the hotkey and opening the reaction
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -66,8 +66,9 @@ const PostOptions = (props: Props): JSX.Element => {
|
||||
const [showActionsMenu, setShowActionsMenu] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const locationToUse = props.location === 'RHS_COMMENT' ? Locations.RHS_ROOT : props.location;
|
||||
if (props.isLastPost &&
|
||||
(props.shortcutReactToLastPostEmittedFrom === props.location) &&
|
||||
(props.shortcutReactToLastPostEmittedFrom === locationToUse) &&
|
||||
props.isPostHeaderVisible) {
|
||||
toggleEmojiPicker();
|
||||
props.actions.emitShortcutReactToLastPostFrom(Locations.NO_WHERE);
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @module makrdownHotkeyHelpers
|
||||
* consolidate testing of similar behavior across components
|
||||
*/
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
/**
|
||||
* @param {string} [input] text input
|
||||
* @param {int} [start] selection's start index
|
||||
* @param {int} [end] selection's end index
|
||||
* @param {object} [keycode] Keycode constant associated with key press
|
||||
* @return {object} keydown event object
|
||||
*/
|
||||
|
||||
export function makeSelectionEvent(input, start, end) {
|
||||
return {
|
||||
preventDefault: jest.fn(),
|
||||
target: {
|
||||
selectionStart: start,
|
||||
selectionEnd: end,
|
||||
value: input,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeMarkdownHotkeyEvent(input, start, end, keycode, altKey = false, targetId = 'root') {
|
||||
return {
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
ctrlKey: true,
|
||||
altKey,
|
||||
key: keycode[0],
|
||||
keyCode: keycode[1],
|
||||
target: {
|
||||
selectionStart: start,
|
||||
selectionEnd: end,
|
||||
value: input,
|
||||
id: targetId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [input] text input
|
||||
* @param {int} [start] selection's start index
|
||||
* @param {int} [end] selection's end index
|
||||
* @return {object} keydown event object
|
||||
*/
|
||||
export function makeBoldHotkeyEvent(input, start, end) {
|
||||
return makeMarkdownHotkeyEvent(input, start, end, Constants.KeyCodes.B);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [input] text input
|
||||
* @param {int} [start] selection's start index
|
||||
* @param {int} [end] selection's end index
|
||||
* @return {object} keydown event object
|
||||
*/
|
||||
export function makeItalicHotkeyEvent(input, start, end) {
|
||||
return makeMarkdownHotkeyEvent(input, start, end, Constants.KeyCodes.I);
|
||||
}
|
||||
|
||||
function makeLinkHotKeyWithoutAltKeyEvent(input, start, end, targetId) {
|
||||
return makeMarkdownHotkeyEvent(input, start, end, Constants.KeyCodes.K, false, targetId);
|
||||
}
|
||||
|
||||
function makeLinkHotKeyEvent(input, start, end) {
|
||||
return makeMarkdownHotkeyEvent(input, start, end, Constants.KeyCodes.K, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* helper to test markdown hotkeys on key down behavior common to many textarea inputs
|
||||
* @param {function} generateInstance - single paramater "value" of the initial value
|
||||
* @param {function} initRefs - React Component instance and setSelectionRange function
|
||||
* @param {function} getValue - single parameter for the React Component instance
|
||||
* NOTE: runs Jest tests
|
||||
*/
|
||||
export function testComponentForMarkdownHotkeys(generateInstance, initRefs, find, getValue, intlInjected = true, targetId) {
|
||||
const shallowRender = intlInjected ? shallowWithIntl : shallow;
|
||||
test('component adds bold markdown', () => {
|
||||
// "Fafda" is selected with ctrl + B hotkey
|
||||
const input = 'Jalebi Fafda & Sambharo';
|
||||
const e = makeBoldHotkeyEvent(input, 7, 12);
|
||||
|
||||
const instance = shallowRender(generateInstance(input));
|
||||
|
||||
const setSelectionRange = jest.fn();
|
||||
initRefs(instance, setSelectionRange);
|
||||
|
||||
find(instance).props().onKeyDown?.(e);
|
||||
find(instance).props().handleKeyDown?.(e);
|
||||
expect(getValue(instance)).toBe('Jalebi **Fafda** & Sambharo');
|
||||
expect(setSelectionRange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('component adds italic markdown', () => {
|
||||
// "Fafda" is selected with ctrl + I hotkey
|
||||
const input = 'Jalebi Fafda & Sambharo';
|
||||
const e = makeItalicHotkeyEvent(input, 7, 12);
|
||||
|
||||
const instance = shallowRender(generateInstance(input));
|
||||
|
||||
const setSelectionRange = jest.fn();
|
||||
initRefs(instance, setSelectionRange);
|
||||
|
||||
find(instance).props().onKeyDown?.(e);
|
||||
find(instance).props().handleKeyDown?.(e);
|
||||
expect(getValue(instance)).toBe('Jalebi *Fafda* & Sambharo');
|
||||
expect(setSelectionRange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('component starts bold markdown', () => {
|
||||
// Nothing is selected, caret is just before "Fafde" with ctrl + B
|
||||
const input = 'Jalebi Fafda & Sambharo';
|
||||
const e = makeBoldHotkeyEvent(input, 7, 7);
|
||||
|
||||
const instance = shallowRender(generateInstance(input));
|
||||
|
||||
const setSelectionRange = jest.fn();
|
||||
initRefs(instance, setSelectionRange);
|
||||
|
||||
find(instance).props().onKeyDown?.(e);
|
||||
find(instance).props().handleKeyDown?.(e);
|
||||
expect(getValue(instance)).toBe('Jalebi ****Fafda & Sambharo');
|
||||
expect(setSelectionRange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('component starts italic markdown', () => {
|
||||
// Nothing is selected, caret is just before "Fafde" with ctrl + B
|
||||
const input = 'Jalebi Fafda & Sambharo';
|
||||
const e = makeItalicHotkeyEvent(input, 7, 7);
|
||||
|
||||
const instance = shallowRender(generateInstance(input));
|
||||
|
||||
const setSelectionRange = jest.fn();
|
||||
initRefs(instance, setSelectionRange);
|
||||
|
||||
find(instance).props().onKeyDown?.(e);
|
||||
find(instance).props().handleKeyDown?.(e);
|
||||
expect(getValue(instance)).toBe('Jalebi **Fafda & Sambharo');
|
||||
expect(setSelectionRange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('component adds link markdown with hitting Ctrl + K when something is selected and event target has specific targetId', () => {
|
||||
// "Fafda" is selected with ctrl + K hotkey
|
||||
const input = 'Jalebi Fafda & Sambharo';
|
||||
const e = makeLinkHotKeyWithoutAltKeyEvent(input, 7, 12, targetId);
|
||||
|
||||
const instance = shallowRender(generateInstance(input));
|
||||
|
||||
let selectionStart = -1;
|
||||
let selectionEnd = -1;
|
||||
const setSelectionRange = jest.fn((start, end) => {
|
||||
selectionStart = start;
|
||||
selectionEnd = end;
|
||||
});
|
||||
initRefs(instance, setSelectionRange);
|
||||
|
||||
find(instance).props().onKeyDown?.(e);
|
||||
find(instance).props().handleKeyDown?.(e);
|
||||
expect(getValue(instance)).toBe('Jalebi [Fafda](url) & Sambharo');
|
||||
expect(setSelectionRange).toHaveBeenCalled();
|
||||
expect(selectionStart).toBe(15);
|
||||
expect(selectionEnd).toBe(18);
|
||||
});
|
||||
|
||||
test('component adds link markdown when something is selected', () => {
|
||||
// "Fafda" is selected with ctrl + alt + K hotkey
|
||||
const input = 'Jalebi Fafda & Sambharo';
|
||||
const e = makeLinkHotKeyEvent(input, 7, 12);
|
||||
|
||||
const instance = shallowRender(generateInstance(input));
|
||||
|
||||
let selectionStart = -1;
|
||||
let selectionEnd = -1;
|
||||
const setSelectionRange = jest.fn((start, end) => {
|
||||
selectionStart = start;
|
||||
selectionEnd = end;
|
||||
});
|
||||
initRefs(instance, setSelectionRange);
|
||||
|
||||
find(instance).props().onKeyDown?.(e);
|
||||
find(instance).props().handleKeyDown?.(e);
|
||||
expect(getValue(instance)).toBe('Jalebi [Fafda](url) & Sambharo');
|
||||
expect(setSelectionRange).toHaveBeenCalled();
|
||||
expect(selectionStart).toBe(15);
|
||||
expect(selectionEnd).toBe(18);
|
||||
});
|
||||
|
||||
test('component adds link markdown when cursor is before a word', () => {
|
||||
// Cursor is before "Fafda" with ctrl + alt + K hotkey
|
||||
const input = 'Jalebi Fafda & Sambharo';
|
||||
const e = makeLinkHotKeyEvent(input, 7, 7);
|
||||
|
||||
const instance = shallowRender(generateInstance(input));
|
||||
|
||||
let selectionStart = -1;
|
||||
let selectionEnd = -1;
|
||||
const setSelectionRange = jest.fn((start, end) => {
|
||||
selectionStart = start;
|
||||
selectionEnd = end;
|
||||
});
|
||||
initRefs(instance, setSelectionRange);
|
||||
|
||||
find(instance).props().onKeyDown?.(e);
|
||||
find(instance).props().handleKeyDown?.(e);
|
||||
expect(getValue(instance)).toBe('Jalebi [Fafda](url) & Sambharo');
|
||||
expect(setSelectionRange).toHaveBeenCalled();
|
||||
expect(selectionStart).toBe(15);
|
||||
expect(selectionEnd).toBe(18);
|
||||
});
|
||||
|
||||
test('component adds link markdown when cursor is in a word', () => {
|
||||
// Cursor is after "Fafda" with ctrl + alt + K hotkey
|
||||
const input = 'Jalebi Fafda & Sambharo';
|
||||
const e = makeLinkHotKeyEvent(input, 10, 10);
|
||||
|
||||
const instance = shallowRender(generateInstance(input));
|
||||
|
||||
let selectionStart = -1;
|
||||
let selectionEnd = -1;
|
||||
const setSelectionRange = jest.fn((start, end) => {
|
||||
selectionStart = start;
|
||||
selectionEnd = end;
|
||||
});
|
||||
initRefs(instance, setSelectionRange);
|
||||
|
||||
find(instance).props().onKeyDown?.(e);
|
||||
find(instance).props().handleKeyDown?.(e);
|
||||
expect(getValue(instance)).toBe('Jalebi [Fafda](url) & Sambharo');
|
||||
expect(setSelectionRange).toHaveBeenCalled();
|
||||
expect(selectionStart).toBe(15);
|
||||
expect(selectionEnd).toBe(18);
|
||||
});
|
||||
|
||||
test('component adds link markdown when cursor is after a word', () => {
|
||||
// Cursor is after "Fafda" with ctrl + alt + K hotkey
|
||||
const input = 'Jalebi Fafda & Sambharo';
|
||||
const e = makeLinkHotKeyEvent(input, 12, 12);
|
||||
|
||||
const instance = shallowRender(generateInstance(input));
|
||||
|
||||
let selectionStart = -1;
|
||||
let selectionEnd = -1;
|
||||
const setSelectionRange = jest.fn((start, end) => {
|
||||
selectionStart = start;
|
||||
selectionEnd = end;
|
||||
});
|
||||
initRefs(instance, setSelectionRange);
|
||||
|
||||
find(instance).props().onKeyDown?.(e);
|
||||
find(instance).props().handleKeyDown?.(e);
|
||||
expect(getValue(instance)).toBe('Jalebi [Fafda](url) & Sambharo');
|
||||
expect(setSelectionRange).toHaveBeenCalled();
|
||||
expect(selectionStart).toBe(15);
|
||||
expect(selectionEnd).toBe(18);
|
||||
});
|
||||
|
||||
test('component adds link markdown when cursor is at the end of line', () => {
|
||||
// Cursor is after "Sambharo" with ctrl + alt + K hotkey
|
||||
const input = 'Jalebi Fafda & Sambharo';
|
||||
const e = makeLinkHotKeyEvent(input, 23, 23);
|
||||
|
||||
const instance = shallowRender(generateInstance(input));
|
||||
|
||||
let selectionStart = -1;
|
||||
let selectionEnd = -1;
|
||||
const setSelectionRange = jest.fn((start, end) => {
|
||||
selectionStart = start;
|
||||
selectionEnd = end;
|
||||
});
|
||||
initRefs(instance, setSelectionRange);
|
||||
|
||||
find(instance).props().onKeyDown?.(e);
|
||||
find(instance).props().handleKeyDown?.(e);
|
||||
expect(getValue(instance)).toBe('Jalebi Fafda & Sambharo [](url)');
|
||||
expect(setSelectionRange).toHaveBeenCalled();
|
||||
expect(selectionStart).toBe(25);
|
||||
expect(selectionEnd).toBe(25);
|
||||
});
|
||||
|
||||
test('component removes link markdown', () => {
|
||||
// "Fafda" is selected with ctrl + alt + K hotkey
|
||||
const input = 'Jalebi [Fafda](url) & Sambharo';
|
||||
const e = makeLinkHotKeyEvent(input, 8, 13);
|
||||
|
||||
const instance = shallowRender(generateInstance(input));
|
||||
|
||||
let selectionStart = -1;
|
||||
let selectionEnd = -1;
|
||||
const setSelectionRange = jest.fn((start, end) => {
|
||||
selectionStart = start;
|
||||
selectionEnd = end;
|
||||
});
|
||||
initRefs(instance, setSelectionRange);
|
||||
|
||||
find(instance).props().onKeyDown?.(e);
|
||||
find(instance).props().handleKeyDown?.(e);
|
||||
expect(getValue(instance)).toBe('Jalebi Fafda & Sambharo');
|
||||
expect(setSelectionRange).toHaveBeenCalled();
|
||||
expect(selectionStart).toBe(7);
|
||||
expect(selectionEnd).toBe(12);
|
||||
});
|
||||
}
|
||||
@@ -13,11 +13,12 @@ import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import configureStore from 'store';
|
||||
|
||||
import WebSocketClient from 'client/web_websocket_client';
|
||||
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
|
||||
import mockStore from 'tests/test_store';
|
||||
import {WebSocketContext} from 'utils/use_websocket';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export * from '@testing-library/react';
|
||||
export {userEvent};
|
||||
|
||||
@@ -63,7 +64,9 @@ export const renderWithFullContext = (component: React.ReactNode | React.ReactNo
|
||||
<IntlProvider
|
||||
locale={renderState.locale}
|
||||
>
|
||||
{renderState.component}
|
||||
<WebSocketContext.Provider value={WebSocketClient}>
|
||||
{renderState.component}
|
||||
</WebSocketContext.Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
</Provider>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locations} from './constants';
|
||||
import {execCommandInsertText} from './exec_commands';
|
||||
import {
|
||||
parseHtmlTable,
|
||||
getHtmlTable,
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
isTextUrl,
|
||||
hasPlainText,
|
||||
createFileFromClipboardDataItem,
|
||||
pasteHandler,
|
||||
} from './paste';
|
||||
|
||||
const validClipboardData: any = {
|
||||
@@ -231,6 +234,79 @@ describe('isTextUrl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
jest.mock('utils/exec_commands', () => ({
|
||||
execCommandInsertText: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('pasteHandler', () => {
|
||||
const testCases = [
|
||||
{
|
||||
testName: 'should be able to format a pasted markdown table',
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/html'],
|
||||
getData: () => {
|
||||
return '<table><tr><th>test</th><th>test</th></tr><tr><td>test</td><td>test</td></tr></table>';
|
||||
},
|
||||
},
|
||||
expectedMarkdown: '| test | test |\n| --- | --- |\n| test | test |',
|
||||
},
|
||||
{
|
||||
testName: 'should be able to format a pasted markdown table without headers',
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/html'],
|
||||
getData: () => {
|
||||
return '<table><tr><td>test</td><td>test</td></tr><tr><td>test</td><td>test</td></tr></table>';
|
||||
},
|
||||
},
|
||||
expectedMarkdown: '| test | test |\n| --- | --- |\n| test | test |\n',
|
||||
},
|
||||
{
|
||||
testName: 'should be able to format a pasted hyperlink',
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/html'],
|
||||
getData: () => {
|
||||
return '<a href="https://test.domain">link text</a>';
|
||||
},
|
||||
},
|
||||
expectedMarkdown: '[link text](https://test.domain)',
|
||||
},
|
||||
{
|
||||
testName: 'should be able to format a github codeblock (pasted as a table)',
|
||||
clipboardData: {
|
||||
items: [1],
|
||||
types: ['text/plain', 'text/html'],
|
||||
getData: (type: string) => {
|
||||
if (type === 'text/plain') {
|
||||
return '// a javascript codeblock example\nif (1 > 0) {\n return \'condition is true\';\n}';
|
||||
}
|
||||
return '<table class="highlight tab-size js-file-line-container" data-tab-size="8"><tbody><tr><td id="LC1" class="blob-code blob-code-inner js-file-line"><span class="pl-c"><span class="pl-c">//</span> a javascript codeblock example</span></td></tr><tr><td id="L2" class="blob-num js-line-number" data-line-number="2"> </td><td id="LC2" class="blob-code blob-code-inner js-file-line"><span class="pl-k">if</span> (<span class="pl-c1">1</span> <span class="pl-k">></span> <span class="pl-c1">0</span>) {</td></tr><tr><td id="L3" class="blob-num js-line-number" data-line-number="3"> </td><td id="LC3" class="blob-code blob-code-inner js-file-line"><span class="pl-en">console</span>.<span class="pl-c1">log</span>(<span class="pl-s"><span class="pl-pds">\'</span>condition is true<span class="pl-pds">\'</span></span>);</td></tr><tr><td id="L4" class="blob-num js-line-number" data-line-number="4"> </td><td id="LC4" class="blob-code blob-code-inner js-file-line">}</td></tr></tbody></table>';
|
||||
},
|
||||
},
|
||||
expectedMarkdown: "```\n// a javascript codeblock example\nif (1 > 0) {\n return 'condition is true';\n}\n```",
|
||||
},
|
||||
];
|
||||
|
||||
for (const tc of testCases) {
|
||||
it(tc.testName, () => {
|
||||
const location = Locations.RHS_COMMENT;
|
||||
const event: any = {
|
||||
target: {
|
||||
id: 'reply_textbox',
|
||||
},
|
||||
preventDefault: jest.fn(),
|
||||
clipboardData: tc.clipboardData,
|
||||
};
|
||||
|
||||
pasteHandler(event, location, '', false, 0);
|
||||
|
||||
expect(execCommandInsertText).toHaveBeenCalledWith(tc.expectedMarkdown);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('hasPlainText', () => {
|
||||
test('Should return true when clipboard data has plain text', () => {
|
||||
const clipboardData = {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {isNil} from 'lodash';
|
||||
|
||||
import type {TextboxElement} from 'components/textbox';
|
||||
|
||||
import {Locations} from 'utils/constants';
|
||||
import {execCommandInsertText} from 'utils/exec_commands';
|
||||
import {DEFAULT_PLACEHOLDER_URL} from 'utils/markdown/apply_markdown';
|
||||
import {splitMessageBasedOnCaretPosition, splitMessageBasedOnTextSelection} from 'utils/post_utils';
|
||||
import turndownService from 'utils/turndown';
|
||||
@@ -154,6 +160,43 @@ export function formatMarkdownLinkMessage({message, clipboardData, selectionStar
|
||||
return markdownLink;
|
||||
}
|
||||
|
||||
export function pasteHandler(event: ClipboardEvent, location: string, message: string, isNonFormattedPaste?: boolean, caretPosition?: number) {
|
||||
const {clipboardData, target} = event;
|
||||
|
||||
const textboxId = location === Locations.RHS_COMMENT ? 'reply_textbox' : 'post_textbox';
|
||||
|
||||
if (!clipboardData || !clipboardData.items || !target || (target as TextboxElement)?.id !== textboxId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {selectionStart, selectionEnd} = target as TextboxElement;
|
||||
|
||||
const hasSelection = !isNil(selectionStart) && !isNil(selectionEnd) && selectionStart < selectionEnd;
|
||||
const hasTextUrl = isTextUrl(clipboardData);
|
||||
const hasHTMLLinks = !isNonFormattedPaste && hasHtmlLink(clipboardData);
|
||||
const htmlTable = getHtmlTable(clipboardData);
|
||||
const shouldApplyLinkMarkdown = hasSelection && hasTextUrl;
|
||||
const shouldApplyGithubCodeBlock = htmlTable && isGitHubCodeBlock(htmlTable.className);
|
||||
|
||||
if (!htmlTable && !hasHTMLLinks && !shouldApplyLinkMarkdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// execCommand's insertText' triggers a 'change' event, hence we need not set respective state explicitly.
|
||||
if (shouldApplyLinkMarkdown) {
|
||||
const formattedLink = formatMarkdownLinkMessage({selectionStart, selectionEnd, message, clipboardData});
|
||||
execCommandInsertText(formattedLink);
|
||||
} else if (shouldApplyGithubCodeBlock) {
|
||||
const {formattedCodeBlock} = formatGithubCodePaste({selectionStart, selectionEnd, message, clipboardData});
|
||||
execCommandInsertText(formattedCodeBlock);
|
||||
} else {
|
||||
const {formattedMarkdown} = formatMarkdownMessage(clipboardData, message, caretPosition);
|
||||
execCommandInsertText(formattedMarkdown);
|
||||
}
|
||||
}
|
||||
|
||||
export function createFileFromClipboardDataItem(item: DataTransferItem, fileNamePrefixIfNoName: string): File | null {
|
||||
const file = item.getAsFile();
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user