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
Этот коммит содержится в:
Harshil Sharma
2025-03-18 16:19:18 +05:30
коммит произвёл GitHub
родитель c1d4f5cb9f
Коммит c951a9cfac
13 изменённых файлов: 78 добавлений и 76 удалений

Просмотреть файл

@@ -302,7 +302,10 @@ describe('Keyboard Shortcuts', () => {
cy.uiGetPostTextBox().type('{uparrow}');
// # 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.getLastPost().within(() => {

Просмотреть файл

@@ -732,7 +732,7 @@ describe('Messaging', () => {
cy.get('#edit_textbox').should('be.visible');
// * 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
cy.get(`#searchResult_${postId}`).findByText('Edited').should('exist');

Просмотреть файл

@@ -3,6 +3,7 @@
import {expect, Locator} from '@playwright/test';
import path from 'node:path';
import {waitUntil} from '@e2e-support/test_action';
export default class ChannelsPostCreate {
readonly container: Locator;
@@ -106,6 +107,12 @@ export default class ChannelsPostCreate {
});
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();

Просмотреть файл

@@ -39,7 +39,7 @@ const AdvancedCreateComment = ({
<AdvancedTextEditor
location={Locations.RHS_COMMENT}
channelId={channelId}
postId={rootId}
rootId={rootId}
isThreadView={isThreadView}
placeholder={placeholder}
afterSubmit={afterSubmit}

Просмотреть файл

@@ -22,7 +22,7 @@ const AdvancedCreatePost = () => {
return (
<AdvancedTextEditor
location={Locations.CENTER}
postId={''}
rootId={''}
channelId={currentChannelId}
/>
);

Просмотреть файл

@@ -130,7 +130,7 @@ const baseProps = {
uploadsProgressPercent: {},
currentChannel: initialState.entities.channels.channels.current_channel_id as Channel,
channelId,
postId: '',
rootId: '',
errorClass: null,
serverError: 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', () => {
const props = {
...baseProps,
postId: 'post_id_1',
rootId: 'post_id_1',
isInEditMode: true,
};
@@ -457,7 +457,7 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
);
expect(container.querySelector('#createPostFileDropOverlay')).toBeVisible();
props.postId = 'post_id_1';
props.rootId = 'post_id_1';
rerender(<AdvancedTextEditor {...props}/>);
expect(container.querySelector('#createCommentFileDropOverlay')).toBeVisible();

Просмотреть файл

@@ -9,11 +9,9 @@ import {useDispatch, useSelector} from 'react-redux';
import type {ServerError} from '@mattermost/types/errors';
import type {SchedulingInfo} from '@mattermost/types/schedule_post';
import {FileTypes} from 'mattermost-redux/action_types';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {Permissions} from 'mattermost-redux/constants';
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 {get, getBool, getInt} from 'mattermost-redux/selectors/entities/preferences';
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
@@ -98,7 +96,8 @@ export type Props = {
*/
location: string;
channelId: string;
postId: string;
rootId: string;
postId?: string;
isThreadView?: boolean;
placeholder?: string;
isInEditMode?: boolean;
@@ -118,6 +117,7 @@ export type Props = {
const AdvancedTextEditor = ({
location,
channelId,
rootId,
postId,
isThreadView = false,
placeholder,
@@ -146,7 +146,7 @@ const AdvancedTextEditor = ({
textboxId = AdvancedTextEditorTextboxIds.Default;
}
const isRHS = Boolean(postId && !isThreadView);
const isRHS = isThreadView ? false : Boolean(rootId) || location === Locations.RHS_COMMENT;
const getFormattingBarPreferenceName = () => {
let name: string;
@@ -164,7 +164,7 @@ const AdvancedTextEditor = ({
const channelDisplayName = channel?.display_name || '';
const channelType = channel?.type || '';
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 maxPostSize = useSelector((state: GlobalState) => parseInt(getConfig(state).MaxPostSize || '', 10) || Constants.DEFAULT_CHARACTER_LIMIT);
const canUploadFiles = useSelector((state: GlobalState) => canUploadFilesAccordingToConfig(getConfig(state)));
@@ -188,7 +188,7 @@ const AdvancedTextEditor = ({
});
const showSendTutorialTip = useSelector((state: GlobalState) => {
// We don't show the tutorial tip neither on RHS nor Thread view
if (postId) {
if (rootId) {
return false;
}
const config = getConfig(state);
@@ -202,7 +202,6 @@ const AdvancedTextEditor = ({
return enableTutorial && (tutorialStep === tourStep);
});
const postFileIds = useSelector((state: GlobalState) => getFilesIdsForPost(state, postId));
const editorActionsRef = useRef<HTMLDivElement>(null);
const editorBodyRef = useRef<HTMLDivElement>(null);
@@ -235,8 +234,8 @@ const AdvancedTextEditor = ({
}, []);
const emitTypingEvent = useCallback(() => {
GlobalActions.emitLocalUserTypingEvent(channelId, postId);
}, [channelId, postId]);
GlobalActions.emitLocalUserTypingEvent(channelId, rootId);
}, [channelId, rootId]);
const handleDraftChange = useCallback((draftToChange: PostDraft, options: {instant?: boolean; show?: boolean} = {instant: false, show: false}) => {
if (saveDraftFrame.current) {
@@ -307,12 +306,12 @@ const AdvancedTextEditor = ({
}]));
}, [dispatch, currentUserId, getFormattingBarPreferenceName, isFormattingBarHidden]);
useOrientationHandler(textboxRef, postId);
useOrientationHandler(textboxRef, rootId);
const pluginItems = usePluginItems(draft, textboxRef, handleDraftChange);
const focusTextbox = useTextboxFocus(textboxRef, channelId, isRHS, canPost);
const [attachmentPreview, fileUploadJSX] = useUploadFiles(
draft,
postId,
rootId,
channelId,
isThreadView,
storedDrafts,
@@ -348,7 +347,7 @@ const AdvancedTextEditor = ({
draft,
postError,
channelId,
postId,
rootId,
serverError,
lastBlurAt,
focusTextbox,
@@ -360,6 +359,7 @@ const AdvancedTextEditor = ({
afterSubmit,
undefined,
isInEditMode,
postId,
);
const handleSubmitWithErrorHandling = useCallback((submittingDraft?: PostDraft, schedulingInfo?: SchedulingInfo, options?: CreatePostOptions) => {
@@ -383,30 +383,10 @@ const AdvancedTextEditor = ({
createAt: 0,
updateAt: 0,
channelId,
rootId: postId,
rootId,
metadata: {},
});
}, [handleDraftChange, channelId, postId]);
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]);
}, [handleDraftChange, channelId, rootId]);
const handleSubmitWrapper = useCallback(() => {
const isEmptyPost = isPostDraftEmpty(draft);
@@ -425,17 +405,13 @@ const AdvancedTextEditor = ({
return;
}
if (isInEditMode) {
handleFileChangesOnSave(draft);
}
handleSubmitWithErrorHandling();
}, [dispatch, draft, handleFileChangesOnSave, handleSubmitWithErrorHandling, isInEditMode, isRHS]);
}, [dispatch, draft, handleSubmitWithErrorHandling, isInEditMode, isRHS]);
const [handleKeyDown, postMsgKeyPress] = useKeyHandler(
draft,
channelId,
postId,
rootId,
caretPosition,
isValidPersistentNotifications,
location,
@@ -583,11 +559,11 @@ const AdvancedTextEditor = ({
useEffect(() => {
setShowPreview(false);
setServerError(null);
}, [channelId, postId]);
}, [channelId, rootId]);
// Remove uploads in progress on mount
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) {
return draft;
}
@@ -626,7 +602,7 @@ const AdvancedTextEditor = ({
handleDraftChange(draftRef.current, {instant: true, show: true});
}
};
}, [channelId, postId]);
}, [channelId, rootId]);
const disableSendButton = Boolean(isDisabled || (!draft.message.trim().length && !draft.fileInfos.length)) || !isValidPersistentNotifications;
const sendButton = readOnlyChannel || isInEditMode ? null : (
@@ -647,7 +623,7 @@ const AdvancedTextEditor = ({
let createMessage;
if (placeholder) {
createMessage = placeholder;
} else if (!postId && !isDisabled) {
} else if (!rootId && !isDisabled) {
createMessage = formatMessage(
{
id: 'create_post.write',
@@ -750,9 +726,9 @@ const AdvancedTextEditor = ({
return (
<form
id={postId ? undefined : 'create_post'}
data-testid={postId ? undefined : 'create-post'}
className={(!postId && !fullWidthTextBox) ? 'center' : undefined}
id={rootId ? undefined : 'create_post'}
data-testid={rootId ? undefined : 'create-post'}
className={(!rootId && !fullWidthTextBox) ? 'center' : undefined}
onSubmit={handleSubmitWithEvent}
>
{canPost && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0) && (
@@ -764,7 +740,7 @@ const AdvancedTextEditor = ({
channelId={channelId}
teammateDisplayName={teammateDisplayName}
location={location}
postId={postId}
postId={rootId}
/>
)}
<div
@@ -824,7 +800,7 @@ const AdvancedTextEditor = ({
preview={showPreview}
badConnection={badConnection}
useChannelMentions={useChannelMentions}
rootId={postId}
rootId={rootId}
onWidthChange={handleWidthChange}
isInEditMode={isInEditMode}
/>
@@ -876,7 +852,7 @@ const AdvancedTextEditor = ({
errorClass={errorClass}
serverError={serverError}
channelId={channelId}
postId={postId}
rootId={rootId}
noArgumentHandleSubmit={handleSubmitWrapper}
isInEditMode={isInEditMode}
/>

Просмотреть файл

@@ -17,7 +17,7 @@ interface Props {
errorClass: string | null;
serverError: ServerError & {submittedMessage?: string} | null;
channelId: Channel['id'];
postId: Post['id'];
rootId: Post['id'];
noArgumentHandleSubmit: () => void;
isInEditMode: boolean;
}
@@ -27,7 +27,7 @@ export default function Footer({
errorClass,
serverError,
channelId,
postId,
rootId,
noArgumentHandleSubmit,
isInEditMode,
}: Props) {
@@ -52,7 +52,7 @@ export default function Footer({
{!isInEditMode && (
<MsgTyping
channelId={channelId}
postId={postId}
rootId={rootId}
/>
)}
</div>

Просмотреть файл

@@ -12,6 +12,7 @@ import {FileTypes} from 'mattermost-redux/action_types';
import {getChannelTimezones} from 'mattermost-redux/actions/channels';
import {Permissions} from 'mattermost-redux/constants';
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 {getPost} from 'mattermost-redux/selectors/entities/posts';
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
@@ -57,7 +58,7 @@ const useSubmit = (
draft: PostDraft,
postError: React.ReactNode,
channelId: string,
postId: string,
rootId: string,
serverError: (ServerError & { submittedMessage?: string }) | null,
lastBlurAt: React.MutableRefObject<number>,
focusTextbox: (forceFocust?: boolean) => void,
@@ -69,6 +70,7 @@ const useSubmit = (
afterSubmit?: (response: SubmitPostReturnType) => void,
skipCommands?: boolean,
isInEditMode?: boolean,
postId?: string,
): [
(submittingDraft?: PostDraft, schedulingInfo?: SchedulingInfo, options?: CreatePostOptions) => void,
string | null,
@@ -77,6 +79,8 @@ const useSubmit = (
const dispatch = useDispatch();
const postFileIds = useSelector((state: GlobalState) => getFilesIdsForPost(state, postId || ''));
const isDraftSubmitting = useRef(false);
const [errorClass, setErrorClass] = useState<string | null>(null);
const isDirectOrGroup = useSelector((state: GlobalState) => {
@@ -92,10 +96,10 @@ const useSubmit = (
});
const isRootDeleted = useSelector((state: GlobalState) => {
if (!postId) {
if (!rootId) {
return false;
}
const post = getPost(state, postId);
const post = getPost(state, rootId);
if (!post || post.delete_at || post.state === 'DELETED') {
return true;
}
@@ -191,7 +195,7 @@ const useSubmit = (
createAt: 0,
updateAt: 0,
channelId,
rootId: postId,
rootId,
}, {instant: true});
} catch (err: unknown) {
if (isServerError(err)) {
@@ -209,7 +213,7 @@ const useSubmit = (
return;
}
if (!postId && !schedulingInfo) {
if (!rootId && !schedulingInfo) {
dispatch(scrollPostListToBottom());
}
@@ -230,7 +234,7 @@ const useSubmit = (
skipCommands,
afterSubmit,
afterOptimisticSubmit,
postId,
rootId,
showPostDeletedModal,
handleDraftChange,
channelId,
@@ -238,12 +242,24 @@ const useSubmit = (
]);
const handleFileChange = useCallback((submittingDraft: PostDraft) => {
// sets the updated data for file IDs by post ID part
dispatch({
type: FileTypes.RECEIVED_FILES_FOR_POST,
data: submittingDraft.fileInfos,
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) => {
// 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 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}`;
return (
@@ -32,7 +31,8 @@ export default function EditPost() {
<AdvancedTextEditor
location={location}
channelId={channelId}
postId={rootId}
rootId={editingPostDetailsAndPost.post.root_id}
postId={editingPostDetailsAndPost.post.id}
isInEditMode={true}
storageKey={storageKey}
placeholder={formatMessage({id: 'edit_post.editPost', defaultMessage: 'Edit the post...'})}

Просмотреть файл

@@ -12,14 +12,14 @@ import MsgTyping from './msg_typing';
type OwnProps = {
channelId: string;
postId: string;
rootId: string;
};
function makeMapStateToProps() {
const getUsersTypingByChannelAndPost = makeGetUsersTypingByChannelAndPost();
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 {
typingUsers,

Просмотреть файл

@@ -10,7 +10,7 @@ describe('components/MsgTyping', () => {
const baseProps = {
typingUsers: [],
channelId: 'test',
postId: '',
rootId: '',
userStartedTyping: jest.fn(),
userStoppedTyping: jest.fn(),
};

Просмотреть файл

@@ -11,7 +11,7 @@ import {useWebSocket} from 'utils/use_websocket';
type Props = {
channelId: string;
postId: string;
rootId: string;
typingUsers: string[];
userStartedTyping: (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 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());
}
} else if (msg.event === SocketEvents.POSTED) {
@@ -36,11 +36,11 @@ export default function MsgTyping(props: Props) {
const rootId = post.root_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());
}
}
}, [props.channelId, props.postId, userStartedTyping, userStoppedTyping]),
}, [props.channelId, props.rootId, userStartedTyping, userStoppedTyping]),
});
const getTypingText = () => {