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}
|
badConnection={false}
|
||||||
canPost={false}
|
canPost={false}
|
||||||
canUploadFiles={true}
|
canUploadFiles={true}
|
||||||
|
caretPosition={12}
|
||||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
channelId="g6139tbospd18cmxroesdk3kkc"
|
||||||
|
ctrlSend={false}
|
||||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
||||||
draft={
|
draft={
|
||||||
Object {
|
Object {
|
||||||
@@ -88,7 +90,6 @@ exports[`components/AdvancedCreateComment should match snapshot when cannot post
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -97,9 +98,13 @@ exports[`components/AdvancedCreateComment should match snapshot when cannot post
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="RHS_COMMENT"
|
location="RHS_COMMENT"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message="Test message"
|
message="Test message"
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
@@ -130,7 +135,9 @@ exports[`components/AdvancedCreateComment should match snapshot, comment with me
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={true}
|
canUploadFiles={true}
|
||||||
|
caretPosition={12}
|
||||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
channelId="g6139tbospd18cmxroesdk3kkc"
|
||||||
|
ctrlSend={true}
|
||||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
||||||
draft={
|
draft={
|
||||||
Object {
|
Object {
|
||||||
@@ -159,7 +166,6 @@ exports[`components/AdvancedCreateComment should match snapshot, comment with me
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -168,9 +174,13 @@ exports[`components/AdvancedCreateComment should match snapshot, comment with me
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="RHS_COMMENT"
|
location="RHS_COMMENT"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message="Test message"
|
message="Test message"
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
@@ -202,7 +212,9 @@ exports[`components/AdvancedCreateComment should match snapshot, emoji picker di
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={true}
|
canUploadFiles={true}
|
||||||
|
caretPosition={12}
|
||||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
channelId="g6139tbospd18cmxroesdk3kkc"
|
||||||
|
ctrlSend={false}
|
||||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
||||||
draft={
|
draft={
|
||||||
Object {
|
Object {
|
||||||
@@ -280,7 +292,6 @@ exports[`components/AdvancedCreateComment should match snapshot, emoji picker di
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -289,9 +300,13 @@ exports[`components/AdvancedCreateComment should match snapshot, emoji picker di
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="RHS_COMMENT"
|
location="RHS_COMMENT"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message="Test message"
|
message="Test message"
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
@@ -322,7 +337,9 @@ exports[`components/AdvancedCreateComment should match snapshot, empty comment 1
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={true}
|
canUploadFiles={true}
|
||||||
|
caretPosition={0}
|
||||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
channelId="g6139tbospd18cmxroesdk3kkc"
|
||||||
|
ctrlSend={true}
|
||||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
||||||
draft={
|
draft={
|
||||||
Object {
|
Object {
|
||||||
@@ -351,7 +368,6 @@ exports[`components/AdvancedCreateComment should match snapshot, empty comment 1
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -360,9 +376,13 @@ exports[`components/AdvancedCreateComment should match snapshot, empty comment 1
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="RHS_COMMENT"
|
location="RHS_COMMENT"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
@@ -394,7 +414,9 @@ exports[`components/AdvancedCreateComment should match snapshot, non-empty messa
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={true}
|
canUploadFiles={true}
|
||||||
|
caretPosition={12}
|
||||||
channelId="g6139tbospd18cmxroesdk3kkc"
|
channelId="g6139tbospd18cmxroesdk3kkc"
|
||||||
|
ctrlSend={false}
|
||||||
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
currentUserId="zaktnt8bpbgu8mb6ez9k64r7sa"
|
||||||
draft={
|
draft={
|
||||||
Object {
|
Object {
|
||||||
@@ -472,7 +494,6 @@ exports[`components/AdvancedCreateComment should match snapshot, non-empty messa
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -481,9 +502,13 @@ exports[`components/AdvancedCreateComment should match snapshot, non-empty messa
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="RHS_COMMENT"
|
location="RHS_COMMENT"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message="Test message"
|
message="Test message"
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
removePreview={[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 type {Props} from 'components/advanced_create_comment/advanced_create_comment';
|
||||||
import AdvancedCreateComment 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 {testComponentForLineBreak} from 'tests/helpers/line_break_helpers';
|
||||||
import {testComponentForMarkdownHotkeys} from 'tests/helpers/markdown_hotkey_helpers.js';
|
|
||||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||||
import {execCommandInsertText} from 'utils/exec_commands';
|
|
||||||
import {TestHelper} from 'utils/test_helper';
|
import {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
import type {PostDraft} from 'types/store/draft';
|
import type {PostDraft} from 'types/store/draft';
|
||||||
@@ -65,8 +62,8 @@ describe('components/AdvancedCreateComment', () => {
|
|||||||
updateCommentDraftWithRootId: jest.fn(),
|
updateCommentDraftWithRootId: jest.fn(),
|
||||||
onSubmit: jest.fn(),
|
onSubmit: jest.fn(),
|
||||||
onResetHistoryIndex: jest.fn(),
|
onResetHistoryIndex: jest.fn(),
|
||||||
onMoveHistoryIndexBack: jest.fn(),
|
moveHistoryIndexBack: jest.fn(),
|
||||||
onMoveHistoryIndexForward: jest.fn(),
|
moveHistoryIndexForward: jest.fn(),
|
||||||
onEditLatestPost: jest.fn(),
|
onEditLatestPost: jest.fn(),
|
||||||
resetCreatePostRequest: jest.fn(),
|
resetCreatePostRequest: jest.fn(),
|
||||||
setShowPreview: jest.fn(),
|
setShowPreview: jest.fn(),
|
||||||
@@ -1208,99 +1205,6 @@ describe('components/AdvancedCreateComment', () => {
|
|||||||
expect(instance.focusTextbox).toHaveBeenCalledTimes(1);
|
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', () => {
|
test('should the RHS thread scroll to bottom one time after mount when props.draft.message is not empty', () => {
|
||||||
const draft: PostDraft = emptyDraft;
|
const draft: PostDraft = emptyDraft;
|
||||||
const scrollToBottom = jest.fn();
|
const scrollToBottom = jest.fn();
|
||||||
@@ -1348,181 +1252,6 @@ describe('components/AdvancedCreateComment', () => {
|
|||||||
expect(scrollToBottom).toBeCalledTimes(2);
|
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', () => {
|
test('should show preview and edit mode, and return focus on preview disable', () => {
|
||||||
const wrapper = shallow<AdvancedCreateComment>(
|
const wrapper = shallow<AdvancedCreateComment>(
|
||||||
<AdvancedCreateComment {...baseProps}/>,
|
<AdvancedCreateComment {...baseProps}/>,
|
||||||
@@ -1552,73 +1281,4 @@ describe('components/AdvancedCreateComment', () => {
|
|||||||
ctrlSend={true}
|
ctrlSend={true}
|
||||||
/>
|
/>
|
||||||
), (instance: any) => instance.state().draft.message, false);
|
), (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 */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
import {isNil} from 'lodash';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type {ChannelMemberCountsByGroup} from '@mattermost/types/channels';
|
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 {Group} from '@mattermost/types/groups';
|
||||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||||
|
|
||||||
|
import {Posts} from 'mattermost-redux/constants';
|
||||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
|
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 type {TextboxClass, TextboxElement} from 'components/textbox';
|
||||||
|
|
||||||
import Constants, {AdvancedTextEditor as AdvancedTextEditorConst, Locations, ModalIdentifiers, Preferences} from 'utils/constants';
|
import Constants, {AdvancedTextEditor as AdvancedTextEditorConst, Locations, ModalIdentifiers, Preferences} from 'utils/constants';
|
||||||
import {execCommandInsertText} from 'utils/exec_commands';
|
|
||||||
import * as Keyboard from 'utils/keyboard';
|
|
||||||
import {
|
import {
|
||||||
applyMarkdown,
|
applyMarkdown,
|
||||||
} from 'utils/markdown/apply_markdown';
|
} from 'utils/markdown/apply_markdown';
|
||||||
import type {
|
import type {
|
||||||
ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
||||||
import {
|
|
||||||
getHtmlTable,
|
|
||||||
hasHtmlLink,
|
|
||||||
formatMarkdownMessage,
|
|
||||||
isGitHubCodeBlock,
|
|
||||||
formatGithubCodePaste,
|
|
||||||
isTextUrl,
|
|
||||||
formatMarkdownLinkMessage,
|
|
||||||
} from 'utils/paste';
|
|
||||||
import {
|
import {
|
||||||
specialMentionsInText,
|
specialMentionsInText,
|
||||||
postMessageOnKeyPress,
|
postMessageOnKeyPress,
|
||||||
@@ -52,7 +41,6 @@ import {
|
|||||||
splitMessageBasedOnCaretPosition,
|
splitMessageBasedOnCaretPosition,
|
||||||
groupsMentionedInText,
|
groupsMentionedInText,
|
||||||
mentionsMinusSpecialMentionsInText,
|
mentionsMinusSpecialMentionsInText,
|
||||||
isWithinCodeBlock,
|
|
||||||
} from 'utils/post_utils';
|
} from 'utils/post_utils';
|
||||||
import * as UserAgent from 'utils/user_agent';
|
import * as UserAgent from 'utils/user_agent';
|
||||||
import * as Utils from 'utils/utils';
|
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 {PostDraft} from 'types/store/draft';
|
||||||
import type {PluginComponent} from 'types/store/plugins';
|
import type {PluginComponent} from 'types/store/plugins';
|
||||||
|
|
||||||
const KeyCodes = Constants.KeyCodes;
|
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
currentTeamId: string;
|
currentTeamId: string;
|
||||||
|
|
||||||
@@ -130,10 +116,10 @@ export type Props = {
|
|||||||
onResetHistoryIndex: () => void;
|
onResetHistoryIndex: () => void;
|
||||||
|
|
||||||
// Called when navigating back through comment message history
|
// Called when navigating back through comment message history
|
||||||
onMoveHistoryIndexBack: () => void;
|
moveHistoryIndexBack: (index: string) => Promise<void>;
|
||||||
|
|
||||||
// Called when navigating forward through comment message history
|
// Called when navigating forward through comment message history
|
||||||
onMoveHistoryIndexForward: () => void;
|
moveHistoryIndexForward: (index: string) => Promise<void>;
|
||||||
|
|
||||||
// Called to initiate editing the user's latest post
|
// Called to initiate editing the user's latest post
|
||||||
onEditLatestPost: () => ActionResult;
|
onEditLatestPost: () => ActionResult;
|
||||||
@@ -206,11 +192,11 @@ type State = {
|
|||||||
uploadsProgressPercent: {[clientID: string]: FilePreviewInfo};
|
uploadsProgressPercent: {[clientID: string]: FilePreviewInfo};
|
||||||
renderScrollbar: boolean;
|
renderScrollbar: boolean;
|
||||||
scrollbarWidth: number;
|
scrollbarWidth: number;
|
||||||
draft?: PostDraft;
|
draft: PostDraft;
|
||||||
rootId?: string;
|
rootId?: string;
|
||||||
messageInHistory?: string;
|
messageInHistory?: string;
|
||||||
createPostErrorId?: string;
|
createPostErrorId?: string;
|
||||||
caretPosition?: number;
|
caretPosition: number;
|
||||||
postError?: React.ReactNode;
|
postError?: React.ReactNode;
|
||||||
errorClass: string | null;
|
errorClass: string | null;
|
||||||
serverError: (ServerError & {submittedMessage?: string}) | null;
|
serverError: (ServerError & {submittedMessage?: string}) | null;
|
||||||
@@ -248,7 +234,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
|||||||
createPostErrorId: props.createPostErrorId,
|
createPostErrorId: props.createPostErrorId,
|
||||||
rootId: props.rootId,
|
rootId: props.rootId,
|
||||||
messageInHistory: props.messageInHistory,
|
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;
|
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,
|
serverError: null,
|
||||||
showFormat: false,
|
showFormat: false,
|
||||||
isFormattingBarHidden: props.isFormattingBarHidden,
|
isFormattingBarHidden: props.isFormattingBarHidden,
|
||||||
caretPosition: props.draft.caretPosition,
|
caretPosition: props.draft.message.length,
|
||||||
|
draft: {...props.draft, uploadsInProgress: []},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.textboxRef = React.createRef();
|
this.textboxRef = React.createRef();
|
||||||
@@ -295,7 +281,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
|||||||
this.focusTextbox();
|
this.focusTextbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('paste', this.pasteHandler);
|
|
||||||
document.addEventListener('keydown', this.focusTextboxIfNecessary);
|
document.addEventListener('keydown', this.focusTextboxIfNecessary);
|
||||||
window.addEventListener('beforeunload', this.saveDraftWithShow);
|
window.addEventListener('beforeunload', this.saveDraftWithShow);
|
||||||
this.getChannelMemberCountsByGroup();
|
this.getChannelMemberCountsByGroup();
|
||||||
@@ -310,7 +295,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.resetCreatePostRequest?.();
|
this.props.resetCreatePostRequest?.();
|
||||||
document.removeEventListener('paste', this.pasteHandler);
|
|
||||||
document.removeEventListener('keydown', this.focusTextboxIfNecessary);
|
document.removeEventListener('keydown', this.focusTextboxIfNecessary);
|
||||||
window.removeEventListener('beforeunload', this.saveDraftWithShow);
|
window.removeEventListener('beforeunload', this.saveDraftWithShow);
|
||||||
this.saveDraftOnUnmount();
|
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 = () => {
|
getChannelMemberCountsByGroup = () => {
|
||||||
const {useLDAPGroupMentions, useCustomGroupMentions, channelId, isTimezoneEnabled, searchAssociatedGroupsForReference, getChannelMemberCountsByGroup, draft, currentTeamId} = this.props;
|
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 = () => {
|
handleNotifyAllConfirmation = () => {
|
||||||
this.doSubmit();
|
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) => {
|
applyMarkdown = (options: ApplyMarkdownOptions) => {
|
||||||
if (this.props.shouldShowPreview) {
|
if (this.props.shouldShowPreview) {
|
||||||
return;
|
return;
|
||||||
@@ -1221,6 +1006,25 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
|||||||
this.lastBlurAt = Date.now();
|
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() {
|
render() {
|
||||||
const draft = this.state.draft!;
|
const draft = this.state.draft!;
|
||||||
|
|
||||||
@@ -1295,7 +1099,6 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
|||||||
handlePostError={this.handlePostError}
|
handlePostError={this.handlePostError}
|
||||||
emitTypingEvent={this.emitTypingEvent}
|
emitTypingEvent={this.emitTypingEvent}
|
||||||
handleMouseUpKeyUp={this.handleMouseUpKeyUp}
|
handleMouseUpKeyUp={this.handleMouseUpKeyUp}
|
||||||
handleKeyDown={this.handleKeyDown}
|
|
||||||
postMsgKeyPress={this.commentMsgKeyPress}
|
postMsgKeyPress={this.commentMsgKeyPress}
|
||||||
handleChange={this.handleChange}
|
handleChange={this.handleChange}
|
||||||
toggleEmojiPicker={this.toggleEmojiPicker}
|
toggleEmojiPicker={this.toggleEmojiPicker}
|
||||||
@@ -1312,6 +1115,13 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
|
|||||||
fileUploadRef={this.fileUploadRef}
|
fileUploadRef={this.fileUploadRef}
|
||||||
isThreadView={this.props.isThreadView}
|
isThreadView={this.props.isThreadView}
|
||||||
additionalControls={pluginItems.filter(Boolean)}
|
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}
|
placeholder={this.props.placeholder}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {ActionCreatorsMapObject, Dispatch} from 'redux';
|
|||||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||||
|
|
||||||
import {getChannelTimezones, getChannelMemberCountsByGroup} from 'mattermost-redux/actions/channels';
|
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 {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||||
import {Permissions, Preferences, Posts} from 'mattermost-redux/constants';
|
import {Permissions, Preferences, Posts} from 'mattermost-redux/constants';
|
||||||
import {getAllChannelStats, getChannelMemberCountsByGroup as selectChannelMemberCountsByGroup} from 'mattermost-redux/selectors/entities/channels';
|
import {getAllChannelStats, getChannelMemberCountsByGroup as selectChannelMemberCountsByGroup} from 'mattermost-redux/selectors/entities/channels';
|
||||||
@@ -25,7 +25,6 @@ import {emitShortcutReactToLastPostFrom} from 'actions/post_actions';
|
|||||||
import {
|
import {
|
||||||
clearCommentDraftUploads,
|
clearCommentDraftUploads,
|
||||||
updateCommentDraft,
|
updateCommentDraft,
|
||||||
makeOnMoveHistoryIndex,
|
|
||||||
makeOnSubmit,
|
makeOnSubmit,
|
||||||
makeOnEditLatestPost,
|
makeOnEditLatestPost,
|
||||||
} from 'actions/views/create_comment';
|
} from 'actions/views/create_comment';
|
||||||
@@ -132,8 +131,8 @@ type Actions = {
|
|||||||
updateCommentDraftWithRootId: (rootID: string, draft: PostDraft, save?: boolean) => void;
|
updateCommentDraftWithRootId: (rootID: string, draft: PostDraft, save?: boolean) => void;
|
||||||
onSubmit: (draft: PostDraft, options: {ignoreSlash: boolean}) => void;
|
onSubmit: (draft: PostDraft, options: {ignoreSlash: boolean}) => void;
|
||||||
onResetHistoryIndex: () => void;
|
onResetHistoryIndex: () => void;
|
||||||
onMoveHistoryIndexBack: () => void;
|
moveHistoryIndexBack: (index: string) => Promise<void>;
|
||||||
onMoveHistoryIndexForward: () => void;
|
moveHistoryIndexForward: (index: string) => Promise<void>;
|
||||||
onEditLatestPost: () => ActionResult;
|
onEditLatestPost: () => ActionResult;
|
||||||
resetCreatePostRequest: () => void;
|
resetCreatePostRequest: () => void;
|
||||||
getChannelTimezones: (channelId: string) => Promise<ActionResult>;
|
getChannelTimezones: (channelId: string) => Promise<ActionResult>;
|
||||||
@@ -152,14 +151,6 @@ function makeMapDispatchToProps() {
|
|||||||
draft: PostDraft,
|
draft: PostDraft,
|
||||||
options: {ignoreSlash: boolean},
|
options: {ignoreSlash: boolean},
|
||||||
) => (dispatch: DispatchFunc, getState: () => GlobalState) => Promise<ActionResult | ActionResult[]> | ActionResult;
|
) => (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;
|
let onEditLatestPost: () => ActionFunc;
|
||||||
|
|
||||||
function onResetHistoryIndex() {
|
function onResetHistoryIndex() {
|
||||||
@@ -173,8 +164,6 @@ function makeMapDispatchToProps() {
|
|||||||
return (dispatch: Dispatch, ownProps: OwnProps) => {
|
return (dispatch: Dispatch, ownProps: OwnProps) => {
|
||||||
if (rootId !== ownProps.rootId) {
|
if (rootId !== ownProps.rootId) {
|
||||||
onUpdateCommentDraft = makeOnUpdateCommentDraft(ownProps.rootId, ownProps.channelId);
|
onUpdateCommentDraft = makeOnUpdateCommentDraft(ownProps.rootId, ownProps.channelId);
|
||||||
onMoveHistoryIndexBack = makeOnMoveHistoryIndex(ownProps.rootId, -1);
|
|
||||||
onMoveHistoryIndexForward = makeOnMoveHistoryIndex(ownProps.rootId, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channelId !== ownProps.channelId) {
|
if (channelId !== ownProps.channelId) {
|
||||||
@@ -200,8 +189,8 @@ function makeMapDispatchToProps() {
|
|||||||
updateCommentDraftWithRootId,
|
updateCommentDraftWithRootId,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onResetHistoryIndex,
|
onResetHistoryIndex,
|
||||||
onMoveHistoryIndexBack,
|
moveHistoryIndexBack,
|
||||||
onMoveHistoryIndexForward,
|
moveHistoryIndexForward,
|
||||||
onEditLatestPost,
|
onEditLatestPost,
|
||||||
resetCreatePostRequest,
|
resetCreatePostRequest,
|
||||||
getChannelTimezones,
|
getChannelTimezones,
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ exports[`components/advanced_create_post Show tutorial 1`] = `
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -62,7 +64,6 @@ exports[`components/advanced_create_post Show tutorial 1`] = `
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -71,13 +72,18 @@ exports[`components/advanced_create_post Show tutorial 1`] = `
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
@@ -109,7 +115,9 @@ exports[`components/advanced_create_post should match snapshot for center textbo
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -158,7 +166,6 @@ exports[`components/advanced_create_post should match snapshot for center textbo
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -167,13 +174,18 @@ exports[`components/advanced_create_post should match snapshot for center textbo
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
@@ -205,7 +217,9 @@ exports[`components/advanced_create_post should match snapshot when cannot post
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={false}
|
canPost={false}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -254,7 +268,6 @@ exports[`components/advanced_create_post should match snapshot when cannot post
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -263,13 +276,18 @@ exports[`components/advanced_create_post should match snapshot when cannot post
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
@@ -301,7 +319,9 @@ exports[`components/advanced_create_post should match snapshot when file upload
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -350,7 +370,6 @@ exports[`components/advanced_create_post should match snapshot when file upload
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -359,13 +378,18 @@ exports[`components/advanced_create_post should match snapshot when file upload
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
@@ -397,7 +421,9 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -446,7 +472,6 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -455,13 +480,18 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
@@ -493,7 +523,9 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -542,7 +574,6 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -551,13 +582,18 @@ exports[`components/advanced_create_post should match snapshot, can post; previe
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
@@ -589,7 +625,9 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={false}
|
canPost={false}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -638,7 +676,6 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -647,13 +684,18 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
@@ -685,7 +727,9 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={false}
|
canPost={false}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -734,7 +778,6 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -743,13 +786,18 @@ exports[`components/advanced_create_post should match snapshot, cannot post; pre
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
@@ -781,7 +829,9 @@ exports[`components/advanced_create_post should match snapshot, init 1`] = `
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -830,7 +880,6 @@ exports[`components/advanced_create_post should match snapshot, init 1`] = `
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -839,13 +888,18 @@ exports[`components/advanced_create_post should match snapshot, init 1`] = `
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
@@ -877,7 +931,9 @@ exports[`components/advanced_create_post should match snapshot, post priority di
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -931,7 +987,6 @@ exports[`components/advanced_create_post should match snapshot, post priority di
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -940,13 +995,18 @@ exports[`components/advanced_create_post should match snapshot, post priority di
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
@@ -986,7 +1046,9 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -1035,7 +1097,6 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -1044,13 +1105,18 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
|||||||
handleUploadStart={[Function]}
|
handleUploadStart={[Function]}
|
||||||
hideEmojiPicker={[Function]}
|
hideEmojiPicker={[Function]}
|
||||||
isFormattingBarHidden={false}
|
isFormattingBarHidden={false}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
@@ -1095,7 +1161,9 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
|||||||
badConnection={false}
|
badConnection={false}
|
||||||
canPost={true}
|
canPost={true}
|
||||||
canUploadFiles={false}
|
canUploadFiles={false}
|
||||||
|
caretPosition={0}
|
||||||
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
channelId="owsyt8n43jfxjpzh9np93mx1wa"
|
||||||
|
ctrlSend={false}
|
||||||
currentChannel={
|
currentChannel={
|
||||||
Object {
|
Object {
|
||||||
"create_at": 0,
|
"create_at": 0,
|
||||||
@@ -1149,7 +1217,6 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
|||||||
handleFileUploadChange={[Function]}
|
handleFileUploadChange={[Function]}
|
||||||
handleFileUploadComplete={[Function]}
|
handleFileUploadComplete={[Function]}
|
||||||
handleGifClick={[Function]}
|
handleGifClick={[Function]}
|
||||||
handleKeyDown={[Function]}
|
|
||||||
handleMouseUpKeyUp={[Function]}
|
handleMouseUpKeyUp={[Function]}
|
||||||
handlePostError={[Function]}
|
handlePostError={[Function]}
|
||||||
handleSubmit={[Function]}
|
handleSubmit={[Function]}
|
||||||
@@ -1173,13 +1240,18 @@ exports[`components/advanced_create_post should match snapshot, post priority en
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
loadNextMessage={[Function]}
|
||||||
|
loadPrevMessage={[Function]}
|
||||||
location="CENTER"
|
location="CENTER"
|
||||||
maxPostSize={4000}
|
maxPostSize={4000}
|
||||||
message=""
|
message=""
|
||||||
|
onEditLatestPost={[Function]}
|
||||||
|
onMessageChange={[Function]}
|
||||||
postId=""
|
postId=""
|
||||||
postMsgKeyPress={[Function]}
|
postMsgKeyPress={[Function]}
|
||||||
prefillMessage={[Function]}
|
prefillMessage={[Function]}
|
||||||
removePreview={[Function]}
|
removePreview={[Function]}
|
||||||
|
replyToLastPost={[Function]}
|
||||||
serverError={null}
|
serverError={null}
|
||||||
setShowPreview={[Function]}
|
setShowPreview={[Function]}
|
||||||
shouldShowPreview={false}
|
shouldShowPreview={false}
|
||||||
|
|||||||
@@ -9,23 +9,16 @@ import type {CommandArgs} from '@mattermost/types/integrations';
|
|||||||
import type {Post} from '@mattermost/types/posts';
|
import type {Post} from '@mattermost/types/posts';
|
||||||
import {PostPriority} 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 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 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 type {TextboxElement} from 'components/textbox';
|
||||||
|
|
||||||
import {testComponentForLineBreak} from 'tests/helpers/line_break_helpers';
|
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 Constants, {StoragePrefixes, ModalIdentifiers} from 'utils/constants';
|
||||||
import EmojiMap from 'utils/emoji_map';
|
import EmojiMap from 'utils/emoji_map';
|
||||||
import {execCommandInsertText} from 'utils/exec_commands';
|
|
||||||
import {TestHelper} from 'utils/test_helper';
|
import {TestHelper} from 'utils/test_helper';
|
||||||
import * as Utils from 'utils/utils';
|
|
||||||
|
|
||||||
jest.mock('actions/global_actions', () => ({
|
jest.mock('actions/global_actions', () => ({
|
||||||
emitLocalUserTypingEvent: jest.fn(),
|
emitLocalUserTypingEvent: jest.fn(),
|
||||||
@@ -993,171 +986,6 @@ describe('components/advanced_create_post', () => {
|
|||||||
expect(instance.handleFileUploadChange).toHaveBeenCalledTimes(1);
|
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', () => {
|
it('Show tutorial', () => {
|
||||||
const wrapper = shallow(advancedCreatePost({
|
const wrapper = shallow(advancedCreatePost({
|
||||||
showSendTutorialTip: true,
|
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
|
* 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,
|
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', () => {
|
it('should match snapshot, can post; preview enabled', () => {
|
||||||
const wrapper = shallow(advancedCreatePost({canPost: true}));
|
const wrapper = shallow(advancedCreatePost({canPost: true}));
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
import {isNil} from 'lodash';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type {Channel, ChannelMemberCountsByGroup} from '@mattermost/types/channels';
|
import type {Channel, ChannelMemberCountsByGroup} from '@mattermost/types/channels';
|
||||||
@@ -44,19 +43,9 @@ import Constants, {
|
|||||||
AdvancedTextEditor as AdvancedTextEditorConst,
|
AdvancedTextEditor as AdvancedTextEditorConst,
|
||||||
} from 'utils/constants';
|
} from 'utils/constants';
|
||||||
import type EmojiMap from 'utils/emoji_map';
|
import type EmojiMap from 'utils/emoji_map';
|
||||||
import {execCommandInsertText} from 'utils/exec_commands';
|
|
||||||
import * as Keyboard from 'utils/keyboard';
|
import * as Keyboard from 'utils/keyboard';
|
||||||
import {applyMarkdown} from 'utils/markdown/apply_markdown';
|
import {applyMarkdown} from 'utils/markdown/apply_markdown';
|
||||||
import type {ApplyMarkdownOptions} 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 {
|
import {
|
||||||
containsAtChannel,
|
containsAtChannel,
|
||||||
specialMentionsInText,
|
specialMentionsInText,
|
||||||
@@ -67,7 +56,6 @@ import {
|
|||||||
groupsMentionedInText,
|
groupsMentionedInText,
|
||||||
mentionsMinusSpecialMentionsInText,
|
mentionsMinusSpecialMentionsInText,
|
||||||
hasRequestedPersistentNotifications,
|
hasRequestedPersistentNotifications,
|
||||||
isWithinCodeBlock,
|
|
||||||
} from 'utils/post_utils';
|
} from 'utils/post_utils';
|
||||||
import * as UserAgent from 'utils/user_agent';
|
import * as UserAgent from 'utils/user_agent';
|
||||||
import * as Utils from 'utils/utils';
|
import * as Utils from 'utils/utils';
|
||||||
@@ -325,7 +313,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
|||||||
actions.setShowPreview(false);
|
actions.setShowPreview(false);
|
||||||
actions.clearDraftUploads();
|
actions.clearDraftUploads();
|
||||||
this.focusTextbox();
|
this.focusTextbox();
|
||||||
document.addEventListener('paste', this.pasteHandler);
|
|
||||||
document.addEventListener('keydown', this.documentKeyHandler);
|
document.addEventListener('keydown', this.documentKeyHandler);
|
||||||
window.addEventListener('beforeunload', this.unloadHandler);
|
window.addEventListener('beforeunload', this.unloadHandler);
|
||||||
this.setOrientationListeners();
|
this.setOrientationListeners();
|
||||||
@@ -357,7 +344,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('paste', this.pasteHandler);
|
|
||||||
document.removeEventListener('keydown', this.documentKeyHandler);
|
document.removeEventListener('keydown', this.documentKeyHandler);
|
||||||
window.removeEventListener('beforeunload', this.unloadHandler);
|
window.removeEventListener('beforeunload', this.unloadHandler);
|
||||||
this.removeOrientationListeners();
|
this.removeOrientationListeners();
|
||||||
@@ -928,43 +914,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
|||||||
this.draftsForChannel[channelId] = draft;
|
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 = () => {
|
handleFileUploadChange = () => {
|
||||||
this.focusTextbox();
|
this.focusTextbox();
|
||||||
};
|
};
|
||||||
@@ -1128,11 +1077,9 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
fillMessageFromHistory() {
|
fillMessageFromHistory() {
|
||||||
const lastMessage = this.props.messageInHistoryItem;
|
const lastMessage = this.props.messageInHistoryItem;
|
||||||
if (lastMessage) {
|
this.setState({
|
||||||
this.setState({
|
message: lastMessage || '',
|
||||||
message: lastMessage,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseUpKeyUp = (e: React.MouseEvent | React.KeyboardEvent) => {
|
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) => {
|
editLastPost = (e: React.KeyboardEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -1580,6 +1339,14 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
|||||||
return Object.values(this.getSpecialMentions()).includes(true);
|
return Object.values(this.getSpecialMentions()).includes(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMessageChange = (message: string, callback?: (() => void) | undefined) => {
|
||||||
|
this.handleDraftChange({
|
||||||
|
...this.props.draft,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
this.setState({message}, callback);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {draft, canPost} = this.props;
|
const {draft, canPost} = this.props;
|
||||||
|
|
||||||
@@ -1666,7 +1433,6 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
|||||||
handlePostError={this.handlePostError}
|
handlePostError={this.handlePostError}
|
||||||
emitTypingEvent={this.emitTypingEvent}
|
emitTypingEvent={this.emitTypingEvent}
|
||||||
handleMouseUpKeyUp={this.handleMouseUpKeyUp}
|
handleMouseUpKeyUp={this.handleMouseUpKeyUp}
|
||||||
handleKeyDown={this.handleKeyDown}
|
|
||||||
postMsgKeyPress={this.postMsgKeyPress}
|
postMsgKeyPress={this.postMsgKeyPress}
|
||||||
handleChange={this.handleChange}
|
handleChange={this.handleChange}
|
||||||
toggleEmojiPicker={this.toggleEmojiPicker}
|
toggleEmojiPicker={this.toggleEmojiPicker}
|
||||||
@@ -1707,6 +1473,14 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
|
|||||||
),
|
),
|
||||||
...(pluginItems || []),
|
...(pluginItems || []),
|
||||||
].filter(Boolean)}
|
].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>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import {searchAssociatedGroupsForReference} from 'actions/views/group';
|
|||||||
import {openModal} from 'actions/views/modals';
|
import {openModal} from 'actions/views/modals';
|
||||||
import {selectPostFromRightHandSideSearchByPostId} from 'actions/views/rhs';
|
import {selectPostFromRightHandSideSearchByPostId} from 'actions/views/rhs';
|
||||||
import {setShowPreviewOnCreatePost} from 'actions/views/textbox';
|
import {setShowPreviewOnCreatePost} from 'actions/views/textbox';
|
||||||
import {getEmojiMap, getShortcutReactToLastPostEmittedFrom} from 'selectors/emojis';
|
import {getEmojiMap} from 'selectors/emojis';
|
||||||
import {getCurrentLocale} from 'selectors/i18n';
|
import {getCurrentLocale} from 'selectors/i18n';
|
||||||
import {makeGetChannelDraft, getIsRhsExpanded, getIsRhsOpen} from 'selectors/rhs';
|
import {makeGetChannelDraft, getIsRhsExpanded, getIsRhsOpen} from 'selectors/rhs';
|
||||||
import {connectionErrorCount} from 'selectors/views/system';
|
import {connectionErrorCount} from 'selectors/views/system';
|
||||||
@@ -81,7 +81,6 @@ function makeMapStateToProps() {
|
|||||||
const userIsOutOfOffice = getStatusForUserId(state, currentUserId) === UserStatuses.OUT_OF_OFFICE;
|
const userIsOutOfOffice = getStatusForUserId(state, currentUserId) === UserStatuses.OUT_OF_OFFICE;
|
||||||
const badConnection = connectionErrorCount(state) > 1;
|
const badConnection = connectionErrorCount(state) > 1;
|
||||||
const isTimezoneEnabled = config.ExperimentalTimezone === 'true';
|
const isTimezoneEnabled = config.ExperimentalTimezone === 'true';
|
||||||
const shortcutReactToLastPostEmittedFrom = getShortcutReactToLastPostEmittedFrom(state);
|
|
||||||
const canPost = haveICurrentChannelPermission(state, Permissions.CREATE_POST);
|
const canPost = haveICurrentChannelPermission(state, Permissions.CREATE_POST);
|
||||||
const useChannelMentions = haveICurrentChannelPermission(state, Permissions.USE_CHANNEL_MENTIONS);
|
const useChannelMentions = haveICurrentChannelPermission(state, Permissions.USE_CHANNEL_MENTIONS);
|
||||||
const isLDAPEnabled = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
|
const isLDAPEnabled = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
|
||||||
@@ -128,7 +127,6 @@ function makeMapStateToProps() {
|
|||||||
emojiMap: getEmojiMap(state),
|
emojiMap: getEmojiMap(state),
|
||||||
badConnection,
|
badConnection,
|
||||||
isTimezoneEnabled,
|
isTimezoneEnabled,
|
||||||
shortcutReactToLastPostEmittedFrom,
|
|
||||||
canPost,
|
canPost,
|
||||||
useChannelMentions,
|
useChannelMentions,
|
||||||
shouldShowPreview: showPreviewOnCreatePost(state),
|
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 classNames from 'classnames';
|
||||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
import {useDispatch} from 'react-redux';
|
||||||
|
|
||||||
import {EmoticonHappyOutlineIcon} from '@mattermost/compass-icons/components';
|
import {EmoticonHappyOutlineIcon} from '@mattermost/compass-icons/components';
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
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 {ServerError} from '@mattermost/types/errors';
|
||||||
import type {FileInfo} from '@mattermost/types/files';
|
import type {FileInfo} from '@mattermost/types/files';
|
||||||
|
|
||||||
|
import {emitShortcutReactToLastPostFrom} from 'actions/post_actions';
|
||||||
|
|
||||||
import AutoHeightSwitcher from 'components/common/auto_height_switcher';
|
import AutoHeightSwitcher from 'components/common/auto_height_switcher';
|
||||||
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
|
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
|
||||||
import FilePreview from 'components/file_preview';
|
import FilePreview from 'components/file_preview';
|
||||||
@@ -29,7 +32,11 @@ import Tooltip from 'components/tooltip';
|
|||||||
import {SendMessageTour} from 'components/tours/onboarding_tour';
|
import {SendMessageTour} from 'components/tours/onboarding_tour';
|
||||||
|
|
||||||
import Constants, {Locations} from 'utils/constants';
|
import Constants, {Locations} from 'utils/constants';
|
||||||
|
import * as Keyboard from 'utils/keyboard';
|
||||||
import type {ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
|
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 * as Utils from 'utils/utils';
|
||||||
|
|
||||||
import type {PostDraft} from 'types/store/draft';
|
import type {PostDraft} from 'types/store/draft';
|
||||||
@@ -44,6 +51,8 @@ import ToggleFormattingBar from './toggle_formatting_bar';
|
|||||||
|
|
||||||
import './advanced_text_editor.scss';
|
import './advanced_text_editor.scss';
|
||||||
|
|
||||||
|
const KeyCodes = Constants.KeyCodes;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +87,6 @@ type Props = {
|
|||||||
handlePostError: (postError: React.ReactNode) => void;
|
handlePostError: (postError: React.ReactNode) => void;
|
||||||
emitTypingEvent: () => void;
|
emitTypingEvent: () => void;
|
||||||
handleMouseUpKeyUp: (e: React.MouseEvent<TextboxElement> | React.KeyboardEvent<TextboxElement>) => void;
|
handleMouseUpKeyUp: (e: React.MouseEvent<TextboxElement> | React.KeyboardEvent<TextboxElement>) => void;
|
||||||
handleKeyDown: (e: React.KeyboardEvent<TextboxElement>) => void;
|
|
||||||
postMsgKeyPress: (e: React.KeyboardEvent<TextboxElement>) => void;
|
postMsgKeyPress: (e: React.KeyboardEvent<TextboxElement>) => void;
|
||||||
handleChange: (e: React.ChangeEvent<TextboxElement>) => void;
|
handleChange: (e: React.ChangeEvent<TextboxElement>) => void;
|
||||||
toggleEmojiPicker: () => void;
|
toggleEmojiPicker: () => void;
|
||||||
@@ -101,6 +109,14 @@ type Props = {
|
|||||||
additionalControls?: React.ReactNodeArray;
|
additionalControls?: React.ReactNodeArray;
|
||||||
labels?: React.ReactNode;
|
labels?: React.ReactNode;
|
||||||
disableSend?: boolean;
|
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;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +152,6 @@ const AdvanceTextEditor = ({
|
|||||||
handlePostError,
|
handlePostError,
|
||||||
emitTypingEvent,
|
emitTypingEvent,
|
||||||
handleMouseUpKeyUp,
|
handleMouseUpKeyUp,
|
||||||
handleKeyDown,
|
|
||||||
postMsgKeyPress,
|
postMsgKeyPress,
|
||||||
handleChange,
|
handleChange,
|
||||||
toggleEmojiPicker,
|
toggleEmojiPicker,
|
||||||
@@ -157,6 +172,14 @@ const AdvanceTextEditor = ({
|
|||||||
additionalControls,
|
additionalControls,
|
||||||
labels,
|
labels,
|
||||||
disableSend = false,
|
disableSend = false,
|
||||||
|
ctrlSend,
|
||||||
|
codeBlockOnCtrlEnter,
|
||||||
|
onMessageChange,
|
||||||
|
onEditLatestPost,
|
||||||
|
loadPrevMessage,
|
||||||
|
loadNextMessage,
|
||||||
|
replyToLastPost,
|
||||||
|
caretPosition,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const readOnlyChannel = !canPost;
|
const readOnlyChannel = !canPost;
|
||||||
@@ -173,6 +196,11 @@ const AdvanceTextEditor = ({
|
|||||||
const [showFormattingSpacer, setShowFormattingSpacer] = useState(shouldShowPreview);
|
const [showFormattingSpacer, setShowFormattingSpacer] = useState(shouldShowPreview);
|
||||||
const [keepEditorInFocus, setKeepEditorInFocus] = useState(false);
|
const [keepEditorInFocus, setKeepEditorInFocus] = useState(false);
|
||||||
|
|
||||||
|
const isNonFormattedPaste = useRef(false);
|
||||||
|
const timeoutId = useRef<number>();
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const input = textboxRef.current?.getInputBox();
|
const input = textboxRef.current?.getInputBox();
|
||||||
|
|
||||||
const handleHeightChange = useCallback((height: number, maxHeight: number) => {
|
const handleHeightChange = useCallback((height: number, maxHeight: number) => {
|
||||||
@@ -192,6 +220,8 @@ const AdvanceTextEditor = ({
|
|||||||
setKeepEditorInFocus(true);
|
setKeepEditorInFocus(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const isRHS = location === Locations.RHS_COMMENT;
|
||||||
|
|
||||||
let attachmentPreview = null;
|
let attachmentPreview = null;
|
||||||
if (!readOnlyChannel && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0)) {
|
if (!readOnlyChannel && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0)) {
|
||||||
attachmentPreview = (
|
attachmentPreview = (
|
||||||
@@ -370,6 +400,205 @@ const AdvanceTextEditor = ({
|
|||||||
}
|
}
|
||||||
}, [message, input]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!message) {
|
if (!message) {
|
||||||
handleWidthChange(0);
|
handleWidthChange(0);
|
||||||
|
|||||||
@@ -326,6 +326,8 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
|
|||||||
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
Keyboard.isKeyPressed(e, KeyCodes.ENTER) &&
|
||||||
ctrlOrMetaKeyPressed;
|
ctrlOrMetaKeyPressed;
|
||||||
const markdownLinkKey = Keyboard.isKeyPressed(e, KeyCodes.K);
|
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
|
// listen for line break key combo and insert new line character
|
||||||
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
if (Utils.isUnhandledLineBreakKeyCombo(e)) {
|
||||||
@@ -356,6 +358,10 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
|
|||||||
selectionEnd: e.currentTarget.selectionEnd,
|
selectionEnd: e.currentTarget.selectionEnd,
|
||||||
message: e.currentTarget.value,
|
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);
|
const [showActionsMenu, setShowActionsMenu] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const locationToUse = props.location === 'RHS_COMMENT' ? Locations.RHS_ROOT : props.location;
|
||||||
if (props.isLastPost &&
|
if (props.isLastPost &&
|
||||||
(props.shortcutReactToLastPostEmittedFrom === props.location) &&
|
(props.shortcutReactToLastPostEmittedFrom === locationToUse) &&
|
||||||
props.isPostHeaderVisible) {
|
props.isPostHeaderVisible) {
|
||||||
toggleEmojiPicker();
|
toggleEmojiPicker();
|
||||||
props.actions.emitShortcutReactToLastPostFrom(Locations.NO_WHERE);
|
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 configureStore from 'store';
|
||||||
|
|
||||||
|
import WebSocketClient from 'client/web_websocket_client';
|
||||||
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
|
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
|
||||||
import mockStore from 'tests/test_store';
|
import mockStore from 'tests/test_store';
|
||||||
|
import {WebSocketContext} from 'utils/use_websocket';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
export * from '@testing-library/react';
|
export * from '@testing-library/react';
|
||||||
export {userEvent};
|
export {userEvent};
|
||||||
|
|
||||||
@@ -63,7 +64,9 @@ export const renderWithFullContext = (component: React.ReactNode | React.ReactNo
|
|||||||
<IntlProvider
|
<IntlProvider
|
||||||
locale={renderState.locale}
|
locale={renderState.locale}
|
||||||
>
|
>
|
||||||
{renderState.component}
|
<WebSocketContext.Provider value={WebSocketClient}>
|
||||||
|
{renderState.component}
|
||||||
|
</WebSocketContext.Provider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</Router>
|
</Router>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Locations} from './constants';
|
||||||
|
import {execCommandInsertText} from './exec_commands';
|
||||||
import {
|
import {
|
||||||
parseHtmlTable,
|
parseHtmlTable,
|
||||||
getHtmlTable,
|
getHtmlTable,
|
||||||
@@ -10,6 +12,7 @@ import {
|
|||||||
isTextUrl,
|
isTextUrl,
|
||||||
hasPlainText,
|
hasPlainText,
|
||||||
createFileFromClipboardDataItem,
|
createFileFromClipboardDataItem,
|
||||||
|
pasteHandler,
|
||||||
} from './paste';
|
} from './paste';
|
||||||
|
|
||||||
const validClipboardData: any = {
|
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', () => {
|
describe('hasPlainText', () => {
|
||||||
test('Should return true when clipboard data has plain text', () => {
|
test('Should return true when clipboard data has plain text', () => {
|
||||||
const clipboardData = {
|
const clipboardData = {
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {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 {DEFAULT_PLACEHOLDER_URL} from 'utils/markdown/apply_markdown';
|
||||||
import {splitMessageBasedOnCaretPosition, splitMessageBasedOnTextSelection} from 'utils/post_utils';
|
import {splitMessageBasedOnCaretPosition, splitMessageBasedOnTextSelection} from 'utils/post_utils';
|
||||||
import turndownService from 'utils/turndown';
|
import turndownService from 'utils/turndown';
|
||||||
@@ -154,6 +160,43 @@ export function formatMarkdownLinkMessage({message, clipboardData, selectionStar
|
|||||||
return markdownLink;
|
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 {
|
export function createFileFromClipboardDataItem(item: DataTransferItem, fileNamePrefixIfNoName: string): File | null {
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user