Thread reply edit file handling bug fix (#30470)
* Fixed a bug where escape would delete all typed content * Fixed rootId and postID * fixed tests * Type fix * Fixed a test * Search result edit fix * Removed a test change * threads view isn't RHS * Fixed a cypress test * Fixed a cypress test * Fixed a flaky playwighr test
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c1d4f5cb9f
Коммит
c951a9cfac
@@ -302,7 +302,10 @@ describe('Keyboard Shortcuts', () => {
|
|||||||
cy.uiGetPostTextBox().type('{uparrow}');
|
cy.uiGetPostTextBox().type('{uparrow}');
|
||||||
|
|
||||||
// # Add some text to the previous message and save
|
// # Add some text to the previous message and save
|
||||||
cy.get('#edit_textbox').type('Test').type('{enter}');
|
// cy.get('#edit_textbox').type('Test').type('{enter}');
|
||||||
|
cy.get('#edit_textbox').type('Test');
|
||||||
|
cy.wait(TIMEOUTS.ONE_SEC);
|
||||||
|
cy.get('#edit_textbox').type('{enter}');
|
||||||
cy.wait(TIMEOUTS.ONE_SEC);
|
cy.wait(TIMEOUTS.ONE_SEC);
|
||||||
|
|
||||||
cy.getLastPost().within(() => {
|
cy.getLastPost().within(() => {
|
||||||
|
|||||||
@@ -732,7 +732,7 @@ describe('Messaging', () => {
|
|||||||
cy.get('#edit_textbox').should('be.visible');
|
cy.get('#edit_textbox').should('be.visible');
|
||||||
|
|
||||||
// * Update the post message and type ENTER
|
// * Update the post message and type ENTER
|
||||||
cy.get('#edit_textbox', {timeout: TIMEOUTS.FIVE_SEC}).invoke('val', '').type(message2).type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
cy.get('#edit_textbox', {timeout: TIMEOUTS.FIVE_SEC}).invoke('val', '').type(message2).wait(TIMEOUTS.HALF_SEC).type('{enter}').wait(TIMEOUTS.HALF_SEC);
|
||||||
|
|
||||||
// * Post appears in RHS search results, displays Pinned badge
|
// * Post appears in RHS search results, displays Pinned badge
|
||||||
cy.get(`#searchResult_${postId}`).findByText('Edited').should('exist');
|
cy.get(`#searchResult_${postId}`).findByText('Edited').should('exist');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import {expect, Locator} from '@playwright/test';
|
import {expect, Locator} from '@playwright/test';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import {waitUntil} from '@e2e-support/test_action';
|
||||||
|
|
||||||
export default class ChannelsPostCreate {
|
export default class ChannelsPostCreate {
|
||||||
readonly container: Locator;
|
readonly container: Locator;
|
||||||
@@ -106,6 +107,12 @@ export default class ChannelsPostCreate {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.attachmentButton.click();
|
await this.attachmentButton.click();
|
||||||
|
|
||||||
|
// wait for all files to be uploaded
|
||||||
|
await waitUntil(async () => {
|
||||||
|
const attachment = await this.container.locator('.file-preview').count();
|
||||||
|
return attachment === files.length;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sendMessage();
|
await this.sendMessage();
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const AdvancedCreateComment = ({
|
|||||||
<AdvancedTextEditor
|
<AdvancedTextEditor
|
||||||
location={Locations.RHS_COMMENT}
|
location={Locations.RHS_COMMENT}
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
postId={rootId}
|
rootId={rootId}
|
||||||
isThreadView={isThreadView}
|
isThreadView={isThreadView}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
afterSubmit={afterSubmit}
|
afterSubmit={afterSubmit}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const AdvancedCreatePost = () => {
|
|||||||
return (
|
return (
|
||||||
<AdvancedTextEditor
|
<AdvancedTextEditor
|
||||||
location={Locations.CENTER}
|
location={Locations.CENTER}
|
||||||
postId={''}
|
rootId={''}
|
||||||
channelId={currentChannelId}
|
channelId={currentChannelId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const baseProps = {
|
|||||||
uploadsProgressPercent: {},
|
uploadsProgressPercent: {},
|
||||||
currentChannel: initialState.entities.channels.channels.current_channel_id as Channel,
|
currentChannel: initialState.entities.channels.channels.current_channel_id as Channel,
|
||||||
channelId,
|
channelId,
|
||||||
postId: '',
|
rootId: '',
|
||||||
errorClass: null,
|
errorClass: null,
|
||||||
serverError: null,
|
serverError: null,
|
||||||
postError: null,
|
postError: null,
|
||||||
@@ -421,7 +421,7 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
|
|||||||
it('should show @mention warning when a mention exists in the message', () => {
|
it('should show @mention warning when a mention exists in the message', () => {
|
||||||
const props = {
|
const props = {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
postId: 'post_id_1',
|
rootId: 'post_id_1',
|
||||||
isInEditMode: true,
|
isInEditMode: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -457,7 +457,7 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
|
|||||||
);
|
);
|
||||||
expect(container.querySelector('#createPostFileDropOverlay')).toBeVisible();
|
expect(container.querySelector('#createPostFileDropOverlay')).toBeVisible();
|
||||||
|
|
||||||
props.postId = 'post_id_1';
|
props.rootId = 'post_id_1';
|
||||||
rerender(<AdvancedTextEditor {...props}/>);
|
rerender(<AdvancedTextEditor {...props}/>);
|
||||||
expect(container.querySelector('#createCommentFileDropOverlay')).toBeVisible();
|
expect(container.querySelector('#createCommentFileDropOverlay')).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ import {useDispatch, useSelector} from 'react-redux';
|
|||||||
import type {ServerError} from '@mattermost/types/errors';
|
import type {ServerError} from '@mattermost/types/errors';
|
||||||
import type {SchedulingInfo} from '@mattermost/types/schedule_post';
|
import type {SchedulingInfo} from '@mattermost/types/schedule_post';
|
||||||
|
|
||||||
import {FileTypes} from 'mattermost-redux/action_types';
|
|
||||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||||
import {Permissions} from 'mattermost-redux/constants';
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
import {getChannel, makeGetChannel, getDirectChannel} from 'mattermost-redux/selectors/entities/channels';
|
import {getChannel, makeGetChannel, getDirectChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getFilesIdsForPost} from 'mattermost-redux/selectors/entities/files';
|
|
||||||
import {getConfig, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {get, getBool, getInt} from 'mattermost-redux/selectors/entities/preferences';
|
import {get, getBool, getInt} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
@@ -98,7 +96,8 @@ export type Props = {
|
|||||||
*/
|
*/
|
||||||
location: string;
|
location: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
postId: string;
|
rootId: string;
|
||||||
|
postId?: string;
|
||||||
isThreadView?: boolean;
|
isThreadView?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isInEditMode?: boolean;
|
isInEditMode?: boolean;
|
||||||
@@ -118,6 +117,7 @@ export type Props = {
|
|||||||
const AdvancedTextEditor = ({
|
const AdvancedTextEditor = ({
|
||||||
location,
|
location,
|
||||||
channelId,
|
channelId,
|
||||||
|
rootId,
|
||||||
postId,
|
postId,
|
||||||
isThreadView = false,
|
isThreadView = false,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -146,7 +146,7 @@ const AdvancedTextEditor = ({
|
|||||||
textboxId = AdvancedTextEditorTextboxIds.Default;
|
textboxId = AdvancedTextEditorTextboxIds.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRHS = Boolean(postId && !isThreadView);
|
const isRHS = isThreadView ? false : Boolean(rootId) || location === Locations.RHS_COMMENT;
|
||||||
|
|
||||||
const getFormattingBarPreferenceName = () => {
|
const getFormattingBarPreferenceName = () => {
|
||||||
let name: string;
|
let name: string;
|
||||||
@@ -164,7 +164,7 @@ const AdvancedTextEditor = ({
|
|||||||
const channelDisplayName = channel?.display_name || '';
|
const channelDisplayName = channel?.display_name || '';
|
||||||
const channelType = channel?.type || '';
|
const channelType = channel?.type || '';
|
||||||
const isChannelShared = channel?.shared;
|
const isChannelShared = channel?.shared;
|
||||||
const draftFromStore = useSelector((state: GlobalState) => getDraftSelector(state, channelId, postId, storageKey));
|
const draftFromStore = useSelector((state: GlobalState) => getDraftSelector(state, channelId, rootId, storageKey));
|
||||||
const badConnection = useSelector((state: GlobalState) => connectionErrorCount(state) > 1);
|
const badConnection = useSelector((state: GlobalState) => connectionErrorCount(state) > 1);
|
||||||
const maxPostSize = useSelector((state: GlobalState) => parseInt(getConfig(state).MaxPostSize || '', 10) || Constants.DEFAULT_CHARACTER_LIMIT);
|
const maxPostSize = useSelector((state: GlobalState) => parseInt(getConfig(state).MaxPostSize || '', 10) || Constants.DEFAULT_CHARACTER_LIMIT);
|
||||||
const canUploadFiles = useSelector((state: GlobalState) => canUploadFilesAccordingToConfig(getConfig(state)));
|
const canUploadFiles = useSelector((state: GlobalState) => canUploadFilesAccordingToConfig(getConfig(state)));
|
||||||
@@ -188,7 +188,7 @@ const AdvancedTextEditor = ({
|
|||||||
});
|
});
|
||||||
const showSendTutorialTip = useSelector((state: GlobalState) => {
|
const showSendTutorialTip = useSelector((state: GlobalState) => {
|
||||||
// We don't show the tutorial tip neither on RHS nor Thread view
|
// We don't show the tutorial tip neither on RHS nor Thread view
|
||||||
if (postId) {
|
if (rootId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const config = getConfig(state);
|
const config = getConfig(state);
|
||||||
@@ -202,7 +202,6 @@ const AdvancedTextEditor = ({
|
|||||||
|
|
||||||
return enableTutorial && (tutorialStep === tourStep);
|
return enableTutorial && (tutorialStep === tourStep);
|
||||||
});
|
});
|
||||||
const postFileIds = useSelector((state: GlobalState) => getFilesIdsForPost(state, postId));
|
|
||||||
|
|
||||||
const editorActionsRef = useRef<HTMLDivElement>(null);
|
const editorActionsRef = useRef<HTMLDivElement>(null);
|
||||||
const editorBodyRef = useRef<HTMLDivElement>(null);
|
const editorBodyRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -235,8 +234,8 @@ const AdvancedTextEditor = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const emitTypingEvent = useCallback(() => {
|
const emitTypingEvent = useCallback(() => {
|
||||||
GlobalActions.emitLocalUserTypingEvent(channelId, postId);
|
GlobalActions.emitLocalUserTypingEvent(channelId, rootId);
|
||||||
}, [channelId, postId]);
|
}, [channelId, rootId]);
|
||||||
|
|
||||||
const handleDraftChange = useCallback((draftToChange: PostDraft, options: {instant?: boolean; show?: boolean} = {instant: false, show: false}) => {
|
const handleDraftChange = useCallback((draftToChange: PostDraft, options: {instant?: boolean; show?: boolean} = {instant: false, show: false}) => {
|
||||||
if (saveDraftFrame.current) {
|
if (saveDraftFrame.current) {
|
||||||
@@ -307,12 +306,12 @@ const AdvancedTextEditor = ({
|
|||||||
}]));
|
}]));
|
||||||
}, [dispatch, currentUserId, getFormattingBarPreferenceName, isFormattingBarHidden]);
|
}, [dispatch, currentUserId, getFormattingBarPreferenceName, isFormattingBarHidden]);
|
||||||
|
|
||||||
useOrientationHandler(textboxRef, postId);
|
useOrientationHandler(textboxRef, rootId);
|
||||||
const pluginItems = usePluginItems(draft, textboxRef, handleDraftChange);
|
const pluginItems = usePluginItems(draft, textboxRef, handleDraftChange);
|
||||||
const focusTextbox = useTextboxFocus(textboxRef, channelId, isRHS, canPost);
|
const focusTextbox = useTextboxFocus(textboxRef, channelId, isRHS, canPost);
|
||||||
const [attachmentPreview, fileUploadJSX] = useUploadFiles(
|
const [attachmentPreview, fileUploadJSX] = useUploadFiles(
|
||||||
draft,
|
draft,
|
||||||
postId,
|
rootId,
|
||||||
channelId,
|
channelId,
|
||||||
isThreadView,
|
isThreadView,
|
||||||
storedDrafts,
|
storedDrafts,
|
||||||
@@ -348,7 +347,7 @@ const AdvancedTextEditor = ({
|
|||||||
draft,
|
draft,
|
||||||
postError,
|
postError,
|
||||||
channelId,
|
channelId,
|
||||||
postId,
|
rootId,
|
||||||
serverError,
|
serverError,
|
||||||
lastBlurAt,
|
lastBlurAt,
|
||||||
focusTextbox,
|
focusTextbox,
|
||||||
@@ -360,6 +359,7 @@ const AdvancedTextEditor = ({
|
|||||||
afterSubmit,
|
afterSubmit,
|
||||||
undefined,
|
undefined,
|
||||||
isInEditMode,
|
isInEditMode,
|
||||||
|
postId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmitWithErrorHandling = useCallback((submittingDraft?: PostDraft, schedulingInfo?: SchedulingInfo, options?: CreatePostOptions) => {
|
const handleSubmitWithErrorHandling = useCallback((submittingDraft?: PostDraft, schedulingInfo?: SchedulingInfo, options?: CreatePostOptions) => {
|
||||||
@@ -383,30 +383,10 @@ const AdvancedTextEditor = ({
|
|||||||
createAt: 0,
|
createAt: 0,
|
||||||
updateAt: 0,
|
updateAt: 0,
|
||||||
channelId,
|
channelId,
|
||||||
rootId: postId,
|
rootId,
|
||||||
metadata: {},
|
metadata: {},
|
||||||
});
|
});
|
||||||
}, [handleDraftChange, channelId, postId]);
|
}, [handleDraftChange, channelId, rootId]);
|
||||||
|
|
||||||
const handleFileChangesOnSave = useCallback((draft: PostDraft) => {
|
|
||||||
// sets the updated data for file IDs by post ID part
|
|
||||||
dispatch({
|
|
||||||
type: FileTypes.RECEIVED_FILES_FOR_POST,
|
|
||||||
data: draft.fileInfos,
|
|
||||||
postId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// removes the data for the deleted files from store
|
|
||||||
const deletedFileIds = postFileIds.filter((id: string) => !draft.fileInfos.find((file) => file.id === id));
|
|
||||||
if (deletedFileIds) {
|
|
||||||
dispatch({
|
|
||||||
type: FileTypes.REMOVED_FILE,
|
|
||||||
data: {
|
|
||||||
fileIds: deletedFileIds,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [dispatch, postFileIds, postId]);
|
|
||||||
|
|
||||||
const handleSubmitWrapper = useCallback(() => {
|
const handleSubmitWrapper = useCallback(() => {
|
||||||
const isEmptyPost = isPostDraftEmpty(draft);
|
const isEmptyPost = isPostDraftEmpty(draft);
|
||||||
@@ -425,17 +405,13 @@ const AdvancedTextEditor = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInEditMode) {
|
|
||||||
handleFileChangesOnSave(draft);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmitWithErrorHandling();
|
handleSubmitWithErrorHandling();
|
||||||
}, [dispatch, draft, handleFileChangesOnSave, handleSubmitWithErrorHandling, isInEditMode, isRHS]);
|
}, [dispatch, draft, handleSubmitWithErrorHandling, isInEditMode, isRHS]);
|
||||||
|
|
||||||
const [handleKeyDown, postMsgKeyPress] = useKeyHandler(
|
const [handleKeyDown, postMsgKeyPress] = useKeyHandler(
|
||||||
draft,
|
draft,
|
||||||
channelId,
|
channelId,
|
||||||
postId,
|
rootId,
|
||||||
caretPosition,
|
caretPosition,
|
||||||
isValidPersistentNotifications,
|
isValidPersistentNotifications,
|
||||||
location,
|
location,
|
||||||
@@ -583,11 +559,11 @@ const AdvancedTextEditor = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowPreview(false);
|
setShowPreview(false);
|
||||||
setServerError(null);
|
setServerError(null);
|
||||||
}, [channelId, postId]);
|
}, [channelId, rootId]);
|
||||||
|
|
||||||
// Remove uploads in progress on mount
|
// Remove uploads in progress on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(actionOnGlobalItemsWithPrefix(postId ? StoragePrefixes.COMMENT_DRAFT : StoragePrefixes.DRAFT, (_key: string, draft: PostDraft) => {
|
dispatch(actionOnGlobalItemsWithPrefix(rootId ? StoragePrefixes.COMMENT_DRAFT : StoragePrefixes.DRAFT, (_key: string, draft: PostDraft) => {
|
||||||
if (!draft || !draft.uploadsInProgress || draft.uploadsInProgress.length === 0) {
|
if (!draft || !draft.uploadsInProgress || draft.uploadsInProgress.length === 0) {
|
||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
@@ -626,7 +602,7 @@ const AdvancedTextEditor = ({
|
|||||||
handleDraftChange(draftRef.current, {instant: true, show: true});
|
handleDraftChange(draftRef.current, {instant: true, show: true});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [channelId, postId]);
|
}, [channelId, rootId]);
|
||||||
|
|
||||||
const disableSendButton = Boolean(isDisabled || (!draft.message.trim().length && !draft.fileInfos.length)) || !isValidPersistentNotifications;
|
const disableSendButton = Boolean(isDisabled || (!draft.message.trim().length && !draft.fileInfos.length)) || !isValidPersistentNotifications;
|
||||||
const sendButton = readOnlyChannel || isInEditMode ? null : (
|
const sendButton = readOnlyChannel || isInEditMode ? null : (
|
||||||
@@ -647,7 +623,7 @@ const AdvancedTextEditor = ({
|
|||||||
let createMessage;
|
let createMessage;
|
||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
createMessage = placeholder;
|
createMessage = placeholder;
|
||||||
} else if (!postId && !isDisabled) {
|
} else if (!rootId && !isDisabled) {
|
||||||
createMessage = formatMessage(
|
createMessage = formatMessage(
|
||||||
{
|
{
|
||||||
id: 'create_post.write',
|
id: 'create_post.write',
|
||||||
@@ -750,9 +726,9 @@ const AdvancedTextEditor = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
id={postId ? undefined : 'create_post'}
|
id={rootId ? undefined : 'create_post'}
|
||||||
data-testid={postId ? undefined : 'create-post'}
|
data-testid={rootId ? undefined : 'create-post'}
|
||||||
className={(!postId && !fullWidthTextBox) ? 'center' : undefined}
|
className={(!rootId && !fullWidthTextBox) ? 'center' : undefined}
|
||||||
onSubmit={handleSubmitWithEvent}
|
onSubmit={handleSubmitWithEvent}
|
||||||
>
|
>
|
||||||
{canPost && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0) && (
|
{canPost && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0) && (
|
||||||
@@ -764,7 +740,7 @@ const AdvancedTextEditor = ({
|
|||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
teammateDisplayName={teammateDisplayName}
|
teammateDisplayName={teammateDisplayName}
|
||||||
location={location}
|
location={location}
|
||||||
postId={postId}
|
postId={rootId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
@@ -824,7 +800,7 @@ const AdvancedTextEditor = ({
|
|||||||
preview={showPreview}
|
preview={showPreview}
|
||||||
badConnection={badConnection}
|
badConnection={badConnection}
|
||||||
useChannelMentions={useChannelMentions}
|
useChannelMentions={useChannelMentions}
|
||||||
rootId={postId}
|
rootId={rootId}
|
||||||
onWidthChange={handleWidthChange}
|
onWidthChange={handleWidthChange}
|
||||||
isInEditMode={isInEditMode}
|
isInEditMode={isInEditMode}
|
||||||
/>
|
/>
|
||||||
@@ -876,7 +852,7 @@ const AdvancedTextEditor = ({
|
|||||||
errorClass={errorClass}
|
errorClass={errorClass}
|
||||||
serverError={serverError}
|
serverError={serverError}
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
postId={postId}
|
rootId={rootId}
|
||||||
noArgumentHandleSubmit={handleSubmitWrapper}
|
noArgumentHandleSubmit={handleSubmitWrapper}
|
||||||
isInEditMode={isInEditMode}
|
isInEditMode={isInEditMode}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface Props {
|
|||||||
errorClass: string | null;
|
errorClass: string | null;
|
||||||
serverError: ServerError & {submittedMessage?: string} | null;
|
serverError: ServerError & {submittedMessage?: string} | null;
|
||||||
channelId: Channel['id'];
|
channelId: Channel['id'];
|
||||||
postId: Post['id'];
|
rootId: Post['id'];
|
||||||
noArgumentHandleSubmit: () => void;
|
noArgumentHandleSubmit: () => void;
|
||||||
isInEditMode: boolean;
|
isInEditMode: boolean;
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ export default function Footer({
|
|||||||
errorClass,
|
errorClass,
|
||||||
serverError,
|
serverError,
|
||||||
channelId,
|
channelId,
|
||||||
postId,
|
rootId,
|
||||||
noArgumentHandleSubmit,
|
noArgumentHandleSubmit,
|
||||||
isInEditMode,
|
isInEditMode,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
@@ -52,7 +52,7 @@ export default function Footer({
|
|||||||
{!isInEditMode && (
|
{!isInEditMode && (
|
||||||
<MsgTyping
|
<MsgTyping
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
postId={postId}
|
rootId={rootId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {FileTypes} from 'mattermost-redux/action_types';
|
|||||||
import {getChannelTimezones} from 'mattermost-redux/actions/channels';
|
import {getChannelTimezones} from 'mattermost-redux/actions/channels';
|
||||||
import {Permissions} from 'mattermost-redux/constants';
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
import {getChannel, getAllChannelStats} from 'mattermost-redux/selectors/entities/channels';
|
import {getChannel, getAllChannelStats} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {getFilesIdsForPost} from 'mattermost-redux/selectors/entities/files';
|
||||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||||
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
@@ -57,7 +58,7 @@ const useSubmit = (
|
|||||||
draft: PostDraft,
|
draft: PostDraft,
|
||||||
postError: React.ReactNode,
|
postError: React.ReactNode,
|
||||||
channelId: string,
|
channelId: string,
|
||||||
postId: string,
|
rootId: string,
|
||||||
serverError: (ServerError & { submittedMessage?: string }) | null,
|
serverError: (ServerError & { submittedMessage?: string }) | null,
|
||||||
lastBlurAt: React.MutableRefObject<number>,
|
lastBlurAt: React.MutableRefObject<number>,
|
||||||
focusTextbox: (forceFocust?: boolean) => void,
|
focusTextbox: (forceFocust?: boolean) => void,
|
||||||
@@ -69,6 +70,7 @@ const useSubmit = (
|
|||||||
afterSubmit?: (response: SubmitPostReturnType) => void,
|
afterSubmit?: (response: SubmitPostReturnType) => void,
|
||||||
skipCommands?: boolean,
|
skipCommands?: boolean,
|
||||||
isInEditMode?: boolean,
|
isInEditMode?: boolean,
|
||||||
|
postId?: string,
|
||||||
): [
|
): [
|
||||||
(submittingDraft?: PostDraft, schedulingInfo?: SchedulingInfo, options?: CreatePostOptions) => void,
|
(submittingDraft?: PostDraft, schedulingInfo?: SchedulingInfo, options?: CreatePostOptions) => void,
|
||||||
string | null,
|
string | null,
|
||||||
@@ -77,6 +79,8 @@ const useSubmit = (
|
|||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const postFileIds = useSelector((state: GlobalState) => getFilesIdsForPost(state, postId || ''));
|
||||||
|
|
||||||
const isDraftSubmitting = useRef(false);
|
const isDraftSubmitting = useRef(false);
|
||||||
const [errorClass, setErrorClass] = useState<string | null>(null);
|
const [errorClass, setErrorClass] = useState<string | null>(null);
|
||||||
const isDirectOrGroup = useSelector((state: GlobalState) => {
|
const isDirectOrGroup = useSelector((state: GlobalState) => {
|
||||||
@@ -92,10 +96,10 @@ const useSubmit = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isRootDeleted = useSelector((state: GlobalState) => {
|
const isRootDeleted = useSelector((state: GlobalState) => {
|
||||||
if (!postId) {
|
if (!rootId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const post = getPost(state, postId);
|
const post = getPost(state, rootId);
|
||||||
if (!post || post.delete_at || post.state === 'DELETED') {
|
if (!post || post.delete_at || post.state === 'DELETED') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -191,7 +195,7 @@ const useSubmit = (
|
|||||||
createAt: 0,
|
createAt: 0,
|
||||||
updateAt: 0,
|
updateAt: 0,
|
||||||
channelId,
|
channelId,
|
||||||
rootId: postId,
|
rootId,
|
||||||
}, {instant: true});
|
}, {instant: true});
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (isServerError(err)) {
|
if (isServerError(err)) {
|
||||||
@@ -209,7 +213,7 @@ const useSubmit = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!postId && !schedulingInfo) {
|
if (!rootId && !schedulingInfo) {
|
||||||
dispatch(scrollPostListToBottom());
|
dispatch(scrollPostListToBottom());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +234,7 @@ const useSubmit = (
|
|||||||
skipCommands,
|
skipCommands,
|
||||||
afterSubmit,
|
afterSubmit,
|
||||||
afterOptimisticSubmit,
|
afterOptimisticSubmit,
|
||||||
postId,
|
rootId,
|
||||||
showPostDeletedModal,
|
showPostDeletedModal,
|
||||||
handleDraftChange,
|
handleDraftChange,
|
||||||
channelId,
|
channelId,
|
||||||
@@ -238,12 +242,24 @@ const useSubmit = (
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const handleFileChange = useCallback((submittingDraft: PostDraft) => {
|
const handleFileChange = useCallback((submittingDraft: PostDraft) => {
|
||||||
|
// sets the updated data for file IDs by post ID part
|
||||||
dispatch({
|
dispatch({
|
||||||
type: FileTypes.RECEIVED_FILES_FOR_POST,
|
type: FileTypes.RECEIVED_FILES_FOR_POST,
|
||||||
data: submittingDraft.fileInfos,
|
data: submittingDraft.fileInfos,
|
||||||
postId,
|
postId,
|
||||||
});
|
});
|
||||||
}, [dispatch, postId]);
|
|
||||||
|
// removes the data for the deleted files from store
|
||||||
|
const deletedFileIds = postFileIds.filter((id: string) => !submittingDraft.fileInfos.find((file) => file.id === id));
|
||||||
|
if (deletedFileIds) {
|
||||||
|
dispatch({
|
||||||
|
type: FileTypes.REMOVED_FILE,
|
||||||
|
data: {
|
||||||
|
fileIds: deletedFileIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [dispatch, postFileIds, postId]);
|
||||||
|
|
||||||
const setUpdatedFileIds = useCallback((draft: PostDraft) => {
|
const setUpdatedFileIds = useCallback((draft: PostDraft) => {
|
||||||
// new object creation is needed here to support sending a draft with files.
|
// new object creation is needed here to support sending a draft with files.
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export default function EditPost() {
|
|||||||
|
|
||||||
const channelId = editingPostDetailsAndPost.post.channel_id;
|
const channelId = editingPostDetailsAndPost.post.channel_id;
|
||||||
const location = editingPostDetailsAndPost.isRHS ? Locations.RHS_COMMENT : Locations.CENTER;
|
const location = editingPostDetailsAndPost.isRHS ? Locations.RHS_COMMENT : Locations.CENTER;
|
||||||
const rootId = editingPostDetailsAndPost.post.root_id || editingPostDetailsAndPost.post.id || '';
|
|
||||||
const storageKey = `${StoragePrefixes.EDIT_DRAFT}${editingPostDetailsAndPost.post.id}`;
|
const storageKey = `${StoragePrefixes.EDIT_DRAFT}${editingPostDetailsAndPost.post.id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +31,8 @@ export default function EditPost() {
|
|||||||
<AdvancedTextEditor
|
<AdvancedTextEditor
|
||||||
location={location}
|
location={location}
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
postId={rootId}
|
rootId={editingPostDetailsAndPost.post.root_id}
|
||||||
|
postId={editingPostDetailsAndPost.post.id}
|
||||||
isInEditMode={true}
|
isInEditMode={true}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
placeholder={formatMessage({id: 'edit_post.editPost', defaultMessage: 'Edit the post...'})}
|
placeholder={formatMessage({id: 'edit_post.editPost', defaultMessage: 'Edit the post...'})}
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ import MsgTyping from './msg_typing';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
postId: string;
|
rootId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function makeMapStateToProps() {
|
function makeMapStateToProps() {
|
||||||
const getUsersTypingByChannelAndPost = makeGetUsersTypingByChannelAndPost();
|
const getUsersTypingByChannelAndPost = makeGetUsersTypingByChannelAndPost();
|
||||||
|
|
||||||
return function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
return function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||||
const typingUsers = getUsersTypingByChannelAndPost(state, {channelId: ownProps.channelId, postId: ownProps.postId});
|
const typingUsers = getUsersTypingByChannelAndPost(state, {channelId: ownProps.channelId, postId: ownProps.rootId});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
typingUsers,
|
typingUsers,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ describe('components/MsgTyping', () => {
|
|||||||
const baseProps = {
|
const baseProps = {
|
||||||
typingUsers: [],
|
typingUsers: [],
|
||||||
channelId: 'test',
|
channelId: 'test',
|
||||||
postId: '',
|
rootId: '',
|
||||||
userStartedTyping: jest.fn(),
|
userStartedTyping: jest.fn(),
|
||||||
userStoppedTyping: jest.fn(),
|
userStoppedTyping: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {useWebSocket} from 'utils/use_websocket';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
postId: string;
|
rootId: string;
|
||||||
typingUsers: string[];
|
typingUsers: string[];
|
||||||
userStartedTyping: (userId: string, channelId: string, rootId: string, now: number) => void;
|
userStartedTyping: (userId: string, channelId: string, rootId: string, now: number) => void;
|
||||||
userStoppedTyping: (userId: string, channelId: string, rootId: string, now: number) => void;
|
userStoppedTyping: (userId: string, channelId: string, rootId: string, now: number) => void;
|
||||||
@@ -26,7 +26,7 @@ export default function MsgTyping(props: Props) {
|
|||||||
const rootId = msg.data.parent_id;
|
const rootId = msg.data.parent_id;
|
||||||
const userId = msg.data.user_id;
|
const userId = msg.data.user_id;
|
||||||
|
|
||||||
if (props.channelId === channelId && props.postId === rootId) {
|
if (props.channelId === channelId && props.rootId === rootId) {
|
||||||
userStartedTyping(userId, channelId, rootId, Date.now());
|
userStartedTyping(userId, channelId, rootId, Date.now());
|
||||||
}
|
}
|
||||||
} else if (msg.event === SocketEvents.POSTED) {
|
} else if (msg.event === SocketEvents.POSTED) {
|
||||||
@@ -36,11 +36,11 @@ export default function MsgTyping(props: Props) {
|
|||||||
const rootId = post.root_id;
|
const rootId = post.root_id;
|
||||||
const userId = post.user_id;
|
const userId = post.user_id;
|
||||||
|
|
||||||
if (props.channelId === channelId && props.postId === rootId) {
|
if (props.channelId === channelId && props.rootId === rootId) {
|
||||||
userStoppedTyping(userId, channelId, rootId, Date.now());
|
userStoppedTyping(userId, channelId, rootId, Date.now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [props.channelId, props.postId, userStartedTyping, userStoppedTyping]),
|
}, [props.channelId, props.rootId, userStartedTyping, userStoppedTyping]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getTypingText = () => {
|
const getTypingText = () => {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user